@salesforce/afv-skills 1.63.0 → 1.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/agentforce-gpt-email-generation-configure/SKILL.md +312 -0
- package/skills/agentforce-offline-experimentation-run/SKILL.md +253 -0
- package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +796 -0
- package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +111 -0
- package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +1053 -0
- package/skills/agentforce-wizard-ui-forecasting-analytics-components-configure/SKILL.md +64 -0
- package/skills/agentforce-wizard-ui-sales-einstein-components-configure/SKILL.md +46 -0
- package/skills/automation-company-profile-info-query/SKILL.md +51 -0
- package/skills/automation-digital-wallet-configure/SKILL.md +689 -0
- package/skills/automation-flow-coordinate/SKILL.md +95 -0
- package/skills/automation-flow-coordinate/references/elements/action-call/action-call.md +1569 -0
- package/skills/automation-flow-coordinate/references/elements/action-call/conversation-messaging.md +96 -0
- package/skills/automation-flow-coordinate/references/elements/action-call/examples/send-sms-message.json +23 -0
- package/skills/automation-flow-coordinate/references/elements/action-call/examples/send-to-journey-no-fields.json +31 -0
- package/skills/automation-flow-coordinate/references/elements/action-call/send-to-journey.md +179 -0
- package/skills/automation-flow-coordinate/references/elements/exit-rule/examples/event-email-link-click.json +13 -0
- package/skills/automation-flow-coordinate/references/elements/exit-rule/exit-rule.md +83 -0
- package/skills/automation-flow-coordinate/references/elements/forward-to-bot-or-agent/examples/forward-to-bot-or-agent.json +69 -0
- package/skills/automation-flow-coordinate/references/elements/forward-to-bot-or-agent/forward-to-bot-or-agent.md +274 -0
- package/skills/automation-flow-coordinate/references/elements/start/audience-common.md +192 -0
- package/skills/automation-flow-coordinate/references/elements/start/automation-event-triggered.md +130 -0
- package/skills/automation-flow-coordinate/references/elements/start/data-cloud-data-change-triggered.md +92 -0
- package/skills/automation-flow-coordinate/references/elements/start/form-submission-triggered.md +88 -0
- package/skills/automation-flow-coordinate/references/elements/start/segment-triggered.md +152 -0
- package/skills/automation-flow-coordinate/references/elements/start/start.md +539 -0
- package/skills/automation-flow-coordinate/references/elements/subflow/subflow.md +152 -0
- package/skills/automation-flow-coordinate/references/elements/wait/wait.md +497 -0
- package/skills/automation-flow-coordinate/references/recipes/author/author.md +868 -0
- package/skills/automation-flow-coordinate/references/recipes/summarize/element-generic.md +300 -0
- package/skills/automation-flow-coordinate/references/recipes/summarize/reference-resolution.md +292 -0
- package/skills/automation-flow-coordinate/references/recipes/summarize/summarize.md +323 -0
- package/skills/automation-flow-coordinate/references/recipes/testing/examples/debug-output-template.md +109 -0
- package/skills/automation-flow-coordinate/references/recipes/testing/shape.md +219 -0
- package/skills/automation-flow-coordinate/references/recipes/testing/shared-stages/intent-analyzer.md +180 -0
- package/skills/automation-flow-coordinate/references/recipes/testing/workflows/debug.md +175 -0
- package/skills/automation-flow-coordinate/references/recipes/testing/workflows/promote.md +255 -0
- package/skills/automation-flow-coordinate/references/recipes/testing/workflows/save-debug.md +212 -0
- package/skills/automation-flow-coordinate/references/resources/variable/variable.md +171 -0
- package/skills/automation-flow-coordinate/references/shared/anti-patterns.md +786 -0
- package/skills/automation-flow-coordinate/references/shared/end/end.md +267 -0
- package/skills/automation-flow-coordinate/references/shared/flow-type-resolver/eval/README.md +69 -0
- package/skills/automation-flow-coordinate/references/shared/flow-type-resolver/eval/predictions.jsonl +80 -0
- package/skills/automation-flow-coordinate/references/shared/flow-type-resolver/eval/prompts.jsonl +116 -0
- package/skills/automation-flow-coordinate/references/shared/flow-type-resolver/flow-type-resolver.md +321 -0
- package/skills/automation-flow-coordinate/references/shared/flow-type-resolver/taxonomy.json +320 -0
- package/skills/automation-flow-coordinate/references/shared/server-functions.md +597 -0
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +21 -11
- package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +10 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +33 -1
- package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +44 -1
- package/skills/automation-sandbox-post-copy-configure/SKILL.md +132 -124
- package/skills/automation-sandbox-post-copy-configure/references/flow_activation_path.md +151 -0
- package/skills/automation-sandbox-post-copy-configure/references/scheduled_apex_path.md +55 -9
- package/skills/automotive-cloud-appraisal-management-settings-configure/SKILL.md +69 -0
- package/skills/automotive-cloud-dealer-essentials-configure/SKILL.md +78 -0
- package/skills/automotive-cloud-ground-agent-data-pipelines-configure/SKILL.md +373 -0
- package/skills/automotive-cloud-ground-mfg-agent-data-pipelines-configure/SKILL.md +273 -0
- package/skills/automotive-cloud-mfg-agent-data-pipelines-configure/SKILL.md +259 -0
- package/skills/automotive-cloud-mfg-sales-concierge-agent-create/SKILL.md +369 -0
- package/skills/automotive-cloud-mfg-sales-concierge-foundation-configure/SKILL.md +542 -0
- package/skills/automotive-cloud-mfg-sales-concierge-foundation-coordinate/SKILL.md +159 -0
- package/skills/automotive-cloud-mfg-sales-concierge-orchestrator-agent-create/SKILL.md +349 -0
- package/skills/automotive-cloud-mfg-sales-concierge-orchestrator-configure/SKILL.md +505 -0
- package/skills/automotive-cloud-sales-concierge-agent-create/SKILL.md +380 -0
- package/skills/automotive-cloud-sales-concierge-foundation-configure/SKILL.md +567 -0
- package/skills/automotive-cloud-sales-concierge-foundation-coordinate/SKILL.md +159 -0
- package/skills/automotive-cloud-sales-concierge-orchestrator-agent-create/SKILL.md +360 -0
- package/skills/automotive-cloud-sales-concierge-orchestrator-configure/SKILL.md +539 -0
- package/skills/automotive-cloud-sales-concierge-search-grounding-configure/SKILL.md +358 -0
- package/skills/commerce-b2b-merchant-quote-configure/SKILL.md +602 -0
- package/skills/commerce-b2b-store-configure/SKILL.md +251 -0
- package/skills/commerce-credit-memo-create/SKILL.md +139 -0
- package/skills/commerce-delivery-business-hours-configure/SKILL.md +277 -0
- package/skills/commerce-delivery-estimation-apply/SKILL.md +155 -0
- package/skills/commerce-delivery-estimation-configure/SKILL.md +377 -0
- package/skills/commerce-delivery-routing-specs-integrate/SKILL.md +174 -0
- package/skills/commerce-estimate-bopis-query/SKILL.md +279 -0
- package/skills/commerce-estimate-delivery-date-query/SKILL.md +349 -0
- package/skills/commerce-fees-apply/SKILL.md +227 -0
- package/skills/commerce-fulfillment-routing-analyze/SKILL.md +200 -0
- package/skills/commerce-guest-buyer-create/SKILL.md +173 -0
- package/skills/commerce-location-bopis-configure/SKILL.md +378 -0
- package/skills/commerce-location-business-hours-configure/SKILL.md +235 -0
- package/skills/commerce-location-delivery-processing-time-configure/SKILL.md +309 -0
- package/skills/commerce-location-graph-integrate/SKILL.md +131 -0
- package/skills/commerce-location-shipping-carrier-method-configure/SKILL.md +755 -0
- package/skills/commerce-location-temporary-operating-hours-configure/SKILL.md +331 -0
- package/skills/commerce-merchant-buyer-groups-configure/SKILL.md +579 -0
- package/skills/commerce-merchant-buyer-user-onboarding-create/SKILL.md +626 -0
- package/skills/commerce-merchant-catalog-configure/SKILL.md +614 -0
- package/skills/commerce-merchant-extension-provider-configure/SKILL.md +368 -0
- package/skills/commerce-merchant-insights-account-health-churn-analyze/SKILL.md +118 -0
- package/skills/commerce-merchant-insights-inventory-returns-analyze/SKILL.md +65 -0
- package/skills/commerce-merchant-insights-order-shopper-get/SKILL.md +90 -0
- package/skills/commerce-merchant-insights-product-performance-analyze/SKILL.md +72 -0
- package/skills/commerce-merchant-insights-query/SKILL.md +279 -0
- package/skills/commerce-merchant-storefront-guest-access-configure/SKILL.md +656 -0
- package/skills/commerce-order-summary-change-apply/SKILL.md +375 -0
- package/skills/commerce-product-details-get/SKILL.md +363 -0
- package/skills/commerce-product-search/SKILL.md +563 -0
- package/skills/commerce-react-storefront-create/SKILL.md +350 -0
- package/skills/commerce-return-order-summary-create/SKILL.md +513 -0
- package/skills/commerce-search-relevance-analyze/SKILL.md +757 -0
- package/skills/commerce-temporary-processing-time-override-create/SKILL.md +274 -0
- package/skills/communications-initial-setup-configure/SKILL.md +630 -0
- package/skills/communications-multisite-configure/SKILL.md +571 -0
- package/skills/communications-revenue-cloud-settings-configure/SKILL.md +74 -0
- package/skills/communications-smb-fiber-catalog-create/SKILL.md +413 -0
- package/skills/communications-tmf-integrate/SKILL.md +1368 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +54 -107
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +1 -1
- package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +82 -5
- package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +35 -2
- package/skills/consumer-goods-promotion-bo-api-deploy/references/preflight-and-seed-queries.md +149 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +17 -1
- package/skills/consumer-goods-visit-activity-act4-questions-assign/SKILL.md +448 -0
- package/skills/consumer-goods-visit-activity-act5-standard-create/SKILL.md +269 -0
- package/skills/consumer-goods-visit-activity-act6-assortment-configure/SKILL.md +271 -0
- package/skills/consumer-goods-visit-activity-act7-account-scoping-create/SKILL.md +382 -0
- package/skills/consumer-goods-visit-activity-act8-plan-attach-create/SKILL.md +373 -0
- package/skills/consumer-goods-visit-activity-act9-proof-validate/SKILL.md +389 -0
- package/skills/consumer-goods-visit-and-activity-act1-specification-create/SKILL.md +338 -0
- package/skills/consumer-goods-visit-and-activity-act2-template-create/SKILL.md +547 -0
- package/skills/consumer-goods-visit-and-activity-act3-definition-create/SKILL.md +346 -0
- package/skills/consumer-goods-visit-and-activity-configure/SKILL.md +1110 -0
- package/skills/crm-analytics-adoption-insights-configure/SKILL.md +224 -0
- package/skills/crm-analytics-custom-report-type-lightning-configure/SKILL.md +418 -0
- package/skills/crm-analytics-dashboards-configure/SKILL.md +731 -0
- package/skills/crm-analytics-operational-analytics-metadata-bridge-migrate/SKILL.md +233 -0
- package/skills/crm-analytics-report-notifications-ui-configure/SKILL.md +184 -0
- package/skills/crm-analytics-sharing-inheritance-coverage-analyze/SKILL.md +167 -0
- package/skills/crm-analytics-sonic-getting-started-configure/SKILL.md +55 -0
- package/skills/crm-analytics-tableau-next-app-deploy/SKILL.md +1350 -0
- package/skills/crm-analytics-tableau-next-c360-basemodel-validate/SKILL.md +1862 -0
- package/skills/crm-analytics-tableau-next-datacloud-validate/SKILL.md +83 -0
- package/skills/crm-analytics-tableau-next-provisioning-validate/SKILL.md +81 -0
- package/skills/data360-activation-create/SKILL.md +104 -0
- package/skills/data360-activation-external-platform-get/SKILL.md +49 -0
- package/skills/data360-activation-target-create/SKILL.md +93 -0
- package/skills/data360-activation-v2-create/SKILL.md +456 -0
- package/skills/data360-audience-dmo-get/SKILL.md +38 -0
- package/skills/data360-calculated-affinity-create/SKILL.md +82 -0
- package/skills/data360-calculated-insight-create/SKILL.md +148 -0
- package/skills/data360-catalog-metadata-create/SKILL.md +183 -0
- package/skills/data360-code-extension-create/SKILL.md +115 -0
- package/skills/data360-communication-capping-create/SKILL.md +227 -0
- package/skills/data360-connection-create/SKILL.md +491 -0
- package/skills/data360-content-lens-create/SKILL.md +181 -0
- package/skills/data360-data-action-create/SKILL.md +126 -0
- package/skills/data360-data-action-target-create/SKILL.md +181 -0
- package/skills/data360-data-clean-room-create/SKILL.md +238 -0
- package/skills/data360-data-governance-create/SKILL.md +746 -0
- package/skills/data360-data-graph-create/SKILL.md +82 -0
- package/skills/data360-data-harmonized-model-object-reference-create/SKILL.md +38 -0
- package/skills/data360-data-lake-object-create/SKILL.md +207 -0
- package/skills/data360-data-model-object-catalog-get/SKILL.md +49 -0
- package/skills/data360-data-model-object-create/SKILL.md +128 -0
- package/skills/data360-data-model-object-mapping-create/SKILL.md +106 -0
- package/skills/data360-data-space-create/SKILL.md +161 -0
- package/skills/data360-data-streams-create/SKILL.md +324 -0
- package/skills/data360-engagement-signal-get/SKILL.md +38 -0
- package/skills/data360-experiment-create/SKILL.md +82 -0
- package/skills/data360-goal-based-campaign-create/SKILL.md +192 -0
- package/skills/data360-harmonization-process-history-get/SKILL.md +38 -0
- package/skills/data360-harmonized-unstructured-dmo-create/SKILL.md +84 -0
- package/skills/data360-identity-resolution-create/SKILL.md +115 -0
- package/skills/data360-intelligent-document-processing-create/SKILL.md +148 -0
- package/skills/data360-knowledge-graph-construction-create/SKILL.md +93 -0
- package/skills/data360-knowledge-graph-create/SKILL.md +216 -0
- package/skills/data360-knowledge-space-mcp-configure/SKILL.md +475 -0
- package/skills/data360-machine-learning-mcp-configure/SKILL.md +697 -0
- package/skills/data360-multi-org-mcp-configure/SKILL.md +117 -0
- package/skills/data360-secondary-index-mcp-configure/SKILL.md +126 -0
- package/skills/data360-segment-mcp-configure/SKILL.md +159 -0
- package/skills/data360-semantic-calculated-dimension-mcp-configure/SKILL.md +93 -0
- package/skills/data360-semantic-calculated-measurement-mcp-configure/SKILL.md +93 -0
- package/skills/data360-semantic-data-object-mcp-configure/SKILL.md +93 -0
- package/skills/data360-semantic-dimension-mcp-configure/SKILL.md +38 -0
- package/skills/data360-semantic-engine-mcp-configure/SKILL.md +62 -0
- package/skills/data360-semantic-measurement-mcp-configure/SKILL.md +38 -0
- package/skills/data360-semantic-metric-mcp-configure/SKILL.md +93 -0
- package/skills/data360-semantic-model-mcp-configure/SKILL.md +115 -0
- package/skills/data360-standard-mapping-mcp-configure/SKILL.md +38 -0
- package/skills/data360-tenant-lifecycle-mcp-configure/SKILL.md +73 -0
- package/skills/dx-devops-conflict-resolve/SKILL.md +1 -5
- package/skills/dx-devops-pipeline-manage/SKILL.md +1 -5
- package/skills/dx-devops-project-manage/SKILL.md +1 -5
- package/skills/dx-devops-promote/SKILL.md +1 -5
- package/skills/dx-devops-request-status/SKILL.md +1 -5
- package/skills/dx-lifecycle-center/SKILL.md +120 -0
- package/skills/dx-lifecycle-center/examples/workflows.md +175 -0
- package/skills/dx-lifecycle-center/references/connect-api.md +123 -0
- package/skills/dx-lifecycle-center/references/connect-org.md +184 -0
- package/skills/dx-lifecycle-center/references/deploy.md +153 -0
- package/skills/dx-lifecycle-center/references/error-handling.md +21 -0
- package/skills/dx-lifecycle-center/references/hub-org.md +37 -0
- package/skills/dx-lifecycle-center/references/list-orgs.md +99 -0
- package/skills/dx-lifecycle-center/references/request-body.md +75 -0
- package/skills/dx-lifecycle-center/references/troubleshooting.md +31 -0
- package/skills/dx-lifecycle-center/scripts/poll-deploy-status.sh +200 -0
- package/skills/dx-lifecycle-center/scripts/resolve-environment.sh +120 -0
- package/skills/dx-org-devhub-configure/SKILL.md +1 -3
- package/skills/dx-org-shape-manage/SKILL.md +1 -5
- package/skills/dx-org-snapshot-manage/SKILL.md +1 -3
- package/skills/education-cloud-academic-calendar-generate/SKILL.md +1 -3
- package/skills/education-cloud-course-catalog-migrate/SKILL.md +1 -3
- package/skills/education-cloud-domain-configure/SKILL.md +1 -3
- package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -3
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -5
- package/skills/experience-afv-query/SKILL.md +234 -0
- package/skills/experience-agentforce-vibes-in-react-configure/SKILL.md +63 -0
- package/skills/experience-apex-components-configure/SKILL.md +69 -0
- package/skills/experience-apex-pages-configure/SKILL.md +107 -0
- package/skills/experience-app-home-configure/SKILL.md +76 -0
- package/skills/experience-aura-lightning-out-access-configure/SKILL.md +58 -0
- package/skills/experience-briefcase-configure/SKILL.md +287 -0
- package/skills/experience-community-flexi-page-list-create/SKILL.md +74 -0
- package/skills/experience-community-template-definition-list-query/SKILL.md +48 -0
- package/skills/experience-community-theme-definition-list-query/SKILL.md +38 -0
- package/skills/experience-content-assets-configure/SKILL.md +86 -0
- package/skills/experience-custom-domain-create/SKILL.md +727 -0
- package/skills/experience-defer-rendering-workspace-page-setup-configure/SKILL.md +41 -0
- package/skills/experience-field-service-app-builder-configure/SKILL.md +187 -0
- package/skills/experience-flow-category-configure/SKILL.md +117 -0
- package/skills/experience-formula-configure/SKILL.md +358 -0
- package/skills/experience-global-actions-configure/SKILL.md +69 -0
- package/skills/experience-global-publisher-layouts-configure/SKILL.md +79 -0
- package/skills/experience-instrumentation-configure/SKILL.md +64 -0
- package/skills/experience-lex-engagement-configure/SKILL.md +50 -0
- package/skills/experience-lightning-bolt-open/SKILL.md +12 -0
- package/skills/experience-lightning-out-configure/SKILL.md +129 -0
- package/skills/experience-lightning-preview-configure/SKILL.md +67 -0
- package/skills/experience-lightning-types-query/SKILL.md +98 -0
- package/skills/experience-malware-scan-configure/SKILL.md +71 -0
- package/skills/experience-my-salesforce-configure/SKILL.md +126 -0
- package/skills/experience-navigation-menus-configure/SKILL.md +235 -0
- package/skills/experience-notification-types-configure/SKILL.md +206 -0
- package/skills/experience-notifications-configure/SKILL.md +75 -0
- package/skills/experience-record-discovery-query/SKILL.md +227 -0
- package/skills/experience-regenerate-previews-open/SKILL.md +94 -0
- package/skills/experience-sales-cloud-mab-configure/SKILL.md +254 -0
- package/skills/experience-schema-builder-open/SKILL.md +38 -0
- package/skills/experience-simple-record-home-configure/SKILL.md +110 -0
- package/skills/experience-static-resources-configure/SKILL.md +74 -0
- package/skills/experience-ui-bundle-2gp-deploy/SKILL.md +1 -3
- package/skills/experience-ui-bundle-features-generate/SKILL.md +1 -5
- package/skills/experience-ui-bundle-mfa-configure/SKILL.md +1 -3
- package/skills/field-service-data-capture-form-deployer-configure/SKILL.md +23 -2
- package/skills/field-service-data-capture-form-designer-configure/SKILL.md +13 -3
- package/skills/field-service-data-capture-migrate/SKILL.md +1 -3
- package/skills/field-service-mobile-branding-configure/SKILL.md +17 -3
- package/skills/field-service-objective-designer-configure/SKILL.md +72 -379
- package/skills/field-service-prework-brief-deployer-configure/SKILL.md +10 -537
- package/skills/field-service-scheduling-policy-designer-query/SKILL.md +65 -1093
- package/skills/{field-service-setup-orchestrator-get → field-service-setup-coordinate}/SKILL.md +18 -3
- package/skills/field-service-sobject-create-configure/SKILL.md +13 -213
- package/skills/field-service-voice-to-form-configure/SKILL.md +40 -11
- package/skills/field-service-work-plan-configure/SKILL.md +253 -0
- package/skills/field-service-work-rule-designer-configure/SKILL.md +121 -95
- package/skills/fsc-actionable-segmentation-settings-configure/SKILL.md +180 -0
- package/skills/fsc-digital-origination-bre-decision-matrix-foundation-create/SKILL.md +196 -0
- package/skills/fsc-digital-origination-bre-disclosures-consent-configure/SKILL.md +106 -0
- package/skills/fsc-digital-origination-bre-document-upload-configure/SKILL.md +96 -0
- package/skills/fsc-digital-origination-bre-permissions-assign/SKILL.md +111 -0
- package/skills/fsc-digital-origination-bre-setup-coordinate/SKILL.md +250 -0
- package/skills/fsc-digital-origination-bre-stp-eligibility-configure/SKILL.md +122 -0
- package/skills/fsc-digital-origination-coordinate/SKILL.md +653 -0
- package/skills/fsc-digital-origination-demo-run/SKILL.md +225 -0
- package/skills/fsc-digital-origination-execution-guardrails-validate/SKILL.md +112 -0
- package/skills/fsc-digital-origination-initial-setup-configure/SKILL.md +396 -0
- package/skills/fsc-digital-origination-pricing-setup-coordinate/SKILL.md +336 -0
- package/skills/fsc-digital-origination-pricing-setup-data-foundation-create/SKILL.md +208 -0
- package/skills/fsc-digital-origination-pricing-setup-procedure-create/SKILL.md +375 -0
- package/skills/fsc-digital-origination-product-setup-act1-permissions-assign/SKILL.md +83 -0
- package/skills/fsc-digital-origination-product-setup-coordinate/SKILL.md +248 -0
- package/skills/fsc-digital-origination-sample-data-create/SKILL.md +206 -0
- package/skills/fsc-document-checklist-settings-configure/SKILL.md +57 -0
- package/skills/fsc-flexible-hierarchy-configure/SKILL.md +148 -0
- package/skills/fsc-flexible-hierarchy-foundation-configure/SKILL.md +1430 -0
- package/skills/fsc-flexible-hierarchy-graph-config-create/SKILL.md +471 -0
- package/skills/fsc-flexible-hierarchy-rule-based-graph-deploy/SKILL.md +429 -0
- package/skills/fsc-pricing-product-configurator-configure/SKILL.md +150 -0
- package/skills/fsc-pricing-salesforce-omnistudio-switch/SKILL.md +123 -0
- package/skills/fsc-product-act2-catalog-categories-create/SKILL.md +82 -0
- package/skills/fsc-product-act3-attributes-create/SKILL.md +249 -0
- package/skills/fsc-product-act4-classification-assignment-create/SKILL.md +196 -0
- package/skills/fsc-product-act5-considerations-get/SKILL.md +44 -0
- package/skills/fsc-stage-management-apex-flow-deploy/SKILL.md +384 -0
- package/skills/fsc-stage-management-compliant-data-sharing-configure/SKILL.md +144 -0
- package/skills/fsc-stage-management-decision-matrices-create/SKILL.md +107 -0
- package/skills/fsc-stage-management-definition-assignments-create/SKILL.md +189 -0
- package/skills/fsc-stage-management-definition-create/SKILL.md +108 -0
- package/skills/fsc-stage-management-permissions-assign/SKILL.md +143 -0
- package/skills/fsc-stage-management-transitions-configure/SKILL.md +139 -0
- package/skills/health-cloud-appt-booking-config-configure/SKILL.md +135 -0
- package/skills/health-cloud-appt-booking-url-config-configure/SKILL.md +134 -0
- package/skills/health-cloud-care-provider-affl-role-config-configure/SKILL.md +136 -0
- package/skills/health-cloud-care-provider-search-config-configure/SKILL.md +134 -0
- package/skills/health-cloud-care-requests-create/SKILL.md +178 -0
- package/skills/health-cloud-initial-configure/SKILL.md +511 -0
- package/skills/health-cloud-patient-appointment-scheduling-configure/SKILL.md +1374 -0
- package/skills/health-cloud-provider-search-settings-configure/SKILL.md +924 -0
- package/skills/health-cloud-referral-management-coordinate/SKILL.md +513 -0
- package/skills/health-cloud-timeline-configure/SKILL.md +622 -0
- package/skills/health-cloud-virtual-visit-config-configure/SKILL.md +135 -0
- package/skills/industries-account-score-settings-query/SKILL.md +78 -0
- package/skills/industries-actionable-list-data-sources-dmo-configure/SKILL.md +177 -0
- package/skills/industries-ad-product-availability-search/SKILL.md +399 -0
- package/skills/industries-advanced-account-forecast-settings-configure/SKILL.md +628 -0
- package/skills/industries-advanced-therapy-management-settings-configure/SKILL.md +73 -0
- package/skills/industries-atm-settings-starter-configure/SKILL.md +76 -0
- package/skills/industries-batch-document-ai-configure/SKILL.md +196 -0
- package/skills/industries-behavioral-health-app-settings-configure/SKILL.md +71 -0
- package/skills/industries-branch-management-settings-configure/SKILL.md +89 -0
- package/skills/industries-bre-decision-table-access-configure/SKILL.md +367 -0
- package/skills/industries-brokerage-settings-configure/SKILL.md +41 -0
- package/skills/industries-calendar-sync-settings-configure/SKILL.md +89 -0
- package/skills/industries-care-request-settings-configure/SKILL.md +218 -0
- package/skills/industries-case-management-intelligence-settings-configure/SKILL.md +54 -0
- package/skills/industries-catalog-process-attributes-products-configure/SKILL.md +380 -0
- package/skills/industries-catalog-process-fulfillment-configure/SKILL.md +216 -0
- package/skills/industries-catalog-process-intake-configure/SKILL.md +219 -0
- package/skills/industries-catalog-process-knowledge-configure/SKILL.md +153 -0
- package/skills/industries-catalog-process-preprocessor-configure/SKILL.md +168 -0
- package/skills/industries-cds-general-settings-configure/SKILL.md +69 -0
- package/skills/industries-claim-financial-settings-configure/SKILL.md +169 -0
- package/skills/industries-clinical-data-model-settings-configure/SKILL.md +136 -0
- package/skills/industries-clinical-data-model-settings-starter-configure/SKILL.md +105 -0
- package/skills/industries-collection-plan-settings-configure/SKILL.md +132 -0
- package/skills/industries-comms-service-console-settings-configure/SKILL.md +159 -0
- package/skills/industries-compliance-configure/SKILL.md +102 -0
- package/skills/industries-compliance-document-extraction-setup-configure/SKILL.md +76 -0
- package/skills/industries-contact-center-app-layout-configure/SKILL.md +88 -0
- package/skills/industries-contact-center-components-configure/SKILL.md +96 -0
- package/skills/industries-contact-center-path-knowledge-configure/SKILL.md +95 -0
- package/skills/industries-contact-center-user-access-assign/SKILL.md +96 -0
- package/skills/industries-course-waitlist-config-configure/SKILL.md +162 -0
- package/skills/industries-crisis-center-settings-configure/SKILL.md +84 -0
- package/skills/industries-custom-object-settings-configure/SKILL.md +94 -0
- package/skills/industries-data-cloud-integration-configure/SKILL.md +88 -0
- package/skills/industries-data-space-feature-mappings-configure/SKILL.md +245 -0
- package/skills/industries-decision-matrix-builder-configure/SKILL.md +78 -0
- package/skills/industries-decision-table-hbase-configure/SKILL.md +87 -0
- package/skills/industries-decision-table-version-configure/SKILL.md +74 -0
- package/skills/industries-decision-tables-builder-configure/SKILL.md +253 -0
- package/skills/industries-decision-tables-configure/SKILL.md +439 -0
- package/skills/industries-energy-utilities-configure/SKILL.md +251 -0
- package/skills/industries-eu-cloud-multisite-configure/SKILL.md +1008 -0
- package/skills/industries-eu-new-connections-configure/SKILL.md +30 -0
- package/skills/industries-explainability-msg-template-configure/SKILL.md +135 -0
- package/skills/industries-expression-set-message-token-configure/SKILL.md +135 -0
- package/skills/industries-field-service-settings-configure/SKILL.md +82 -0
- package/skills/industries-form-framework-settings-configure/SKILL.md +30 -0
- package/skills/industries-fsc-ai-settings-configure/SKILL.md +57 -0
- package/skills/industries-fsc-association-engine-settings-configure/SKILL.md +172 -0
- package/skills/industries-fsc-b2b-settings-configure/SKILL.md +61 -0
- package/skills/industries-fsc-insurance-settings-configure/SKILL.md +58 -0
- package/skills/industries-fsc-visa-fulfillment-setup-configure/SKILL.md +103 -0
- package/skills/industries-fsc-wealth-agent-settings-configure/SKILL.md +117 -0
- package/skills/industries-fsc-wealth-settings-configure/SKILL.md +99 -0
- package/skills/industries-fundraising-gift-entry-settings-configure/SKILL.md +120 -0
- package/skills/industries-fundraising-rfm-configure/SKILL.md +128 -0
- package/skills/industries-fundraising-settings-configure/SKILL.md +145 -0
- package/skills/industries-fundraising-source-code-gen-configure/SKILL.md +299 -0
- package/skills/industries-google-maps-api-key-settings-configure/SKILL.md +87 -0
- package/skills/industries-grantmaking-intelligence-settings-open/SKILL.md +58 -0
- package/skills/industries-hc-score-category-settings-configure/SKILL.md +167 -0
- package/skills/industries-health-engagement-settings-configure/SKILL.md +218 -0
- package/skills/industries-home-health-agentforce-setup-configure/SKILL.md +131 -0
- package/skills/industries-home-health-bundling-policy-create/SKILL.md +50 -0
- package/skills/industries-home-health-entity-labels-configure/SKILL.md +29 -0
- package/skills/industries-home-health-fsl-package-deploy/SKILL.md +71 -0
- package/skills/industries-home-health-fsl-settings-validate/SKILL.md +27 -0
- package/skills/industries-home-health-guided-setup-care-and-skill-get/SKILL.md +80 -0
- package/skills/industries-home-health-guided-setup-configure/SKILL.md +836 -0
- package/skills/industries-home-health-guided-setup-enable-org-fsl-get/SKILL.md +55 -0
- package/skills/industries-home-health-guided-setup-get-org-ready-get/SKILL.md +61 -0
- package/skills/industries-home-health-licenses-assign/SKILL.md +27 -0
- package/skills/industries-home-health-operating-hours-create/SKILL.md +40 -0
- package/skills/industries-home-health-scheduling-policy-create/SKILL.md +41 -0
- package/skills/industries-home-health-service-objective-create/SKILL.md +22 -0
- package/skills/industries-home-health-skills-create/SKILL.md +31 -0
- package/skills/industries-home-health-users-create/SKILL.md +29 -0
- package/skills/industries-icm-act3-toggles-configure/SKILL.md +198 -0
- package/skills/industries-icm-act5-genai-suggested-assessments-configure/SKILL.md +361 -0
- package/skills/industries-icm-act6-mcg-integration-configure/SKILL.md +550 -0
- package/skills/industries-integrated-care-management-configure/SKILL.md +168 -0
- package/skills/industries-object-enablement-configure/SKILL.md +86 -0
- package/skills/industries-tpm-system-processing-service-configure/SKILL.md +64 -0
- package/skills/insurance-brokerage-agency-billing-configure/SKILL.md +1 -3
- package/skills/integration-apex-settings-configure/SKILL.md +61 -0
- package/skills/marketing-e360-email-archiving-configure/SKILL.md +73 -0
- package/skills/marketing-message-test/SKILL.md +1 -3
- package/skills/nonprofit-fundraising-configure/SKILL.md +240 -0
- package/skills/nonprofit-fundraising-configure/references/gotchas.md +26 -0
- package/skills/nonprofit-fundraising-configure/references/mcp-invocation.md +678 -0
- package/skills/nonprofit-fundraising-configure/references/permission-targeting.md +35 -0
- package/skills/nonprofit-volunteer-management-model-get/SKILL.md +166 -0
- package/skills/nonprofit-volunteer-management-model-get/references/entities.md +315 -0
- package/skills/nonprofit-volunteer-management-model-get/references/relationships.md +175 -0
- package/skills/nonprofit-volunteer-management-model-get/references/themes.md +143 -0
- package/skills/nonprofit-volunteer-management-records-create/SKILL.md +121 -0
- package/skills/nonprofit-volunteer-management-records-create/references/field-guide.md +284 -0
- package/skills/nonprofit-volunteer-management-records-create/references/mcp-invocation.md +248 -0
- package/skills/nonprofit-volunteer-management-records-create/references/review-format.md +140 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/expand-series.py +632 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/render-review-table.py +419 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/tests/test_expand_series.py +690 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/tests/test_render_review_table.py +421 -0
- package/skills/nonprofit-volunteer-management-structure-generate/SKILL.md +119 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/configuration-decisions.md +159 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/discovery-interview.md +137 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/edge-cases.md +48 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/mcp-invocation.md +75 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/plan-output.md +141 -0
- package/skills/platform-custom-field-generate/SKILL.md +1 -0
- package/skills/platform-custom-field-generate/references/advanced-picklists.md +5 -5
- package/skills/platform-custom-metadata-type-generate/SKILL.md +3 -0
- package/skills/platform-custom-object-generate/SKILL.md +4 -0
- package/skills/platform-custom-permissions-configure/SKILL.md +687 -0
- package/skills/platform-custom-setting-generate/SKILL.md +3 -0
- package/skills/platform-datamask-run/SKILL.md +1 -5
- package/skills/platform-label-workbench-import-create/SKILL.md +67 -0
- package/skills/platform-permission-set-generate/SKILL.md +3 -0
- package/skills/platform-privacy-agent-settings-configure/SKILL.md +752 -0
- package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +1 -3
- package/skills/platform-sandbox-configure/SKILL.md +1 -3
- package/skills/platform-value-set-generate/SKILL.md +3 -0
- package/skills/sales-call-scoring-configure/SKILL.md +1 -5
- package/skills/service-agentforce-channel-configure/SKILL.md +1 -3
- package/skills/service-agentforce-human-escalation-configure/SKILL.md +1 -3
- package/skills/service-catalog-template-coordinate/SKILL.md +1 -3
- package/skills/service-concierge-portal-generate/SKILL.md +1 -3
- package/skills/service-email-to-case-configure/SKILL.md +1 -5
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-access-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-bundle-deploy/SKILL.md +1 -5
- package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-discovery-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +1 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-uel-user-create/SKILL.md +1 -9
- package/skills/service-itsm-incident-mgmt-configure/SKILL.md +1 -5
- package/skills/service-itsm-incident-priority-configure/SKILL.md +1 -3
- package/skills/service-itsm-slack-itservice-configure/SKILL.md +1 -3
- package/skills/service-itsm-swarming-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-debug/SKILL.md +1 -3
- package/skills/service-itsm-teams-employee-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-itdesk-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-itservice-configure/SKILL.md +1 -3
- package/skills/service-native-voice-recording-transcription-configure/SKILL.md +1 -3
- package/skills/service-omni-agent-users-create/SKILL.md +1 -3
- package/skills/service-omni-agent-work-sharing-configure/SKILL.md +1 -3
- package/skills/service-omni-attribute-routing-configure/SKILL.md +1 -3
- package/skills/service-omni-base-settings-configure/SKILL.md +1 -3
- package/skills/service-omni-channel-inventory-analyze/SKILL.md +1 -3
- package/skills/service-omni-channel-limits-analyze/SKILL.md +1 -3
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +1 -3
- package/skills/service-omni-command-center-analyze/SKILL.md +1 -3
- package/skills/service-omni-command-center-configure/SKILL.md +1 -3
- package/skills/service-omni-permission-set-assign/SKILL.md +1 -3
- package/skills/service-omni-presence-status-deploy/SKILL.md +1 -3
- package/skills/service-omni-presence-user-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-queue-deploy/SKILL.md +1 -3
- package/skills/service-omni-queue-members-assign/SKILL.md +1 -3
- package/skills/service-omni-queue-routing-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-routing-flow-deploy/SKILL.md +1 -3
- package/skills/service-omni-service-channel-configure/SKILL.md +1 -3
- package/skills/service-omni-sidebar-configure/SKILL.md +1 -3
- package/skills/service-omni-skills-based-routing-configure/SKILL.md +1 -3
- package/skills/service-omni-supervisor-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-supervisor-permset-assign/SKILL.md +1 -3
- package/skills/service-omni-supervisor-surface-deploy/SKILL.md +1 -3
- package/skills/service-omni-supervisor-users-create/SKILL.md +1 -3
- package/skills/service-omni-work-skill-routing-configure/SKILL.md +1 -3
- package/skills/tableau-next-asset-create/README.md +0 -65
|
@@ -0,0 +1,1350 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crm-analytics-tableau-next-app-deploy
|
|
3
|
+
description: "Install a Tableau Next / App-Framework template as a new app on a Salesforce org, end-to-end and self-contained. Scoped to an explicit ALLOWLIST — Service Insights (`sfdc_internal__ServiceInsights`), Sales Insights TN (`sfdc_internal__SalesInsightsTN`), Employee Agent Analytics (`sfdc_internal__Employee_Agent_Analytics`), Service Agent Analytics (`sfdc_internal__Service_Agent_Analytics`), and C360 Semantic Model (`sfdc_internal__C360UnifiedSemanticModel`). Any other template request is refused with a \"not supported\" message. Use when the user says \"install <one of the allowed templates>\", \"provision the ServiceInsights app\", \"spin up Sales Insights TN\", \"install Employee Agent Analytics\", \"install Service Agent Analytics\", or \"install C360 Semantic Model\". This skill performs the entire install workflow itself (allowlist gate → enumerate templates → fetch variables → detect C360 base-model dependency → check readiness → create app → poll status → optional rollback) using `sf api request rest` against the org's `/app-framework/*` endpoints — it does NOT depend on the project-codey MCP tools or the tableau-next-apps-mcp/-beta tools, though it can opportunistically use them if they're connected."
|
|
4
|
+
metadata:
|
|
5
|
+
version: "1.0"
|
|
6
|
+
accessCheck: "(AppTemplatingFramework.hasAppFrameworkAccess) || (DataCloud.enabled) || (UnifiedAnalytics.orgHasTableauOrSemanticLayerEnabled) || (UserPermissions.AppFrameworkManageTemplate)"
|
|
7
|
+
cliTools:
|
|
8
|
+
- tool:
|
|
9
|
+
- "jq"
|
|
10
|
+
semver: ">=1.6"
|
|
11
|
+
- tool:
|
|
12
|
+
- "python3"
|
|
13
|
+
semver: ">=3.8"
|
|
14
|
+
- tool:
|
|
15
|
+
- "sf"
|
|
16
|
+
semver: ">=2.0.0"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# tableau-next-app-install
|
|
20
|
+
|
|
21
|
+
## What this does
|
|
22
|
+
|
|
23
|
+
Drives a full **App-Framework template install** against the configured Salesforce org, entirely on its own. This skill is **self-contained**: it executes each step via `sf api request rest` (the sf-CLI's typed REST wrapper) against the App-Framework Connect API — at whatever API version the org publishes at runtime (see "Discover API version") — so it can run even when the project-codey MCP tools or the tableau-next-apps-mcp / tableau-next-apps-mcp-beta MCP tools are unreachable or unresponsive.
|
|
24
|
+
|
|
25
|
+
The workflow it drives (each step is a direct `sf api request rest` call — no MCP tool required):
|
|
26
|
+
|
|
27
|
+
| Step | Endpoint | Purpose |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| 1 | `GET /app-framework/templates` | Enumerate available templates on the org |
|
|
30
|
+
| 2 | `GET /app-framework/templates/{id}/configuration` | Discover what inputs the template expects at install time (variables live under `.variables` in the response body) |
|
|
31
|
+
| 2b | `GET /app-framework/templates/{id}/assets` | Detect C360 Semantic Model base-model dependency — refuse install if the required C360 app isn't installed on the org (see step 2b) |
|
|
32
|
+
| 3 | `POST /app-framework/templates/{id}/readiness` | Ask the platform "can this template install right now?" |
|
|
33
|
+
| 4 | `POST /app-framework/apps` | Kick off the async install |
|
|
34
|
+
| 5 | `GET /app-framework/apps/{id}` (repeat) | Poll `applicationStatus` until it settles |
|
|
35
|
+
| 6 | `DELETE /app-framework/apps/{id}` | Roll back a failed / unwanted install |
|
|
36
|
+
| 6b | `PUT /app-framework/apps/{id}` | Update / reconfigure an existing app in-place (the API-driven equivalent of App Hub → tile → Update). Used by 2b.5 to auto-remediate a C360 template-version mismatch on the HOME org without requiring the user to run the App Hub wizard by hand. |
|
|
37
|
+
|
|
38
|
+
If the caller has MCP tools connected — either project-codey's five install tools OR the tableau-next-apps-mcp/-beta family — the skill MAY use them as a faster path (they wrap the same endpoints), but it is not required to. See "Fast path (optional)" below.
|
|
39
|
+
|
|
40
|
+
## Supported templates (ALLOWLIST — refuse anything else)
|
|
41
|
+
|
|
42
|
+
**This skill installs ONLY the templates in the list below.** Any request naming a template outside the list — even if the template exists on the org and would install fine — MUST be refused with the exact message documented under "Allowlist gate" and the caller directed to the appropriate scope-owner. Do NOT proceed past step 1 for a non-allowlisted template. Silently substituting a "close match" is a correctness bug.
|
|
43
|
+
|
|
44
|
+
| User-facing name | Accepted apiName(s) | Accepted labels (case-insensitive EXACT match after normalization: lowercased, trimmed, internal whitespace collapsed) |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| Service Insights | `sfdc_internal__ServiceInsights` | `service insights`, `serviceinsights`, `service_insights` |
|
|
47
|
+
| Sales Insights TN | `sfdc_internal__SalesInsightsTN` | `sales insights`, `salesinsights`, `sales_insights`, `sales insights tn`, `salesinsightstn` |
|
|
48
|
+
| Employee Agent Analytics | `sfdc_internal__Employee_Agent_Analytics` | `employee agent analytics`, `employee_agent_analytics`, `employeeagentanalytics` |
|
|
49
|
+
| Service Agent Analytics | `sfdc_internal__Service_Agent_Analytics` | `service agent analytics`, `service_agent_analytics`, `serviceagentanalytics` |
|
|
50
|
+
| C360 Semantic Model | `sfdc_internal__C360UnifiedSemanticModel` | `c360 semantic model`, `c360semanticmodel`, `c360_semantic_model`, `c360 unified semantic model`, `c360unifiedsemanticmodel`, `c360` |
|
|
51
|
+
|
|
52
|
+
**Match discipline: EXACT string equality after normalization — NEVER substring.** A substring implementation would let `install "agent"` silently install one of the Agent Analytics templates, or `install "service agent analytics dashboard"` match Service Agent Analytics against a request that isn't for it. The code below implements exact match; do not "help" by softening it to `contains`.
|
|
53
|
+
|
|
54
|
+
**Note on the bare label `agent analytics`:** with two allowlisted Agent Analytics templates (Employee, Service), the bare label alone is ambiguous — resolved to `AMBIGUOUS_AGENT_ANALYTICS` and the caller is asked to pick.
|
|
55
|
+
|
|
56
|
+
**Canonical apiName resolution — do this BEFORE step 1:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Normalize the user's input (lowercase, collapse whitespace) and match against the allowlist.
|
|
60
|
+
# Case-insensitive; strip leading/trailing whitespace; treat "sfdc_internal__" prefix as optional
|
|
61
|
+
# so users can say "SalesInsightsTN" or "sfdc_internal__SalesInsightsTN" interchangeably.
|
|
62
|
+
resolve_allowed_template() {
|
|
63
|
+
local raw="$1"
|
|
64
|
+
# Normalize once: lowercase, then convert ALL whitespace (tabs, newlines, etc.)
|
|
65
|
+
# to single spaces via `tr -s '[:space:]' ' '`, then trim leading/trailing spaces.
|
|
66
|
+
# `tr -s` squeezes runs, so multi-tab and mixed tab/space also collapse cleanly.
|
|
67
|
+
local norm
|
|
68
|
+
norm=$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | tr -s '[:space:]' ' ' | sed 's/^ //; s/ $//')
|
|
69
|
+
|
|
70
|
+
case "$norm" in
|
|
71
|
+
sfdc_internal__serviceinsights|serviceinsights|"service insights"|service_insights)
|
|
72
|
+
echo "sfdc_internal__ServiceInsights" ;;
|
|
73
|
+
sfdc_internal__salesinsightstn|salesinsightstn|salesinsights|"sales insights"|"sales insights tn"|sales_insights)
|
|
74
|
+
echo "sfdc_internal__SalesInsightsTN" ;;
|
|
75
|
+
sfdc_internal__employee_agent_analytics|employee_agent_analytics|"employee agent analytics"|employeeagentanalytics)
|
|
76
|
+
echo "sfdc_internal__Employee_Agent_Analytics" ;;
|
|
77
|
+
sfdc_internal__service_agent_analytics|service_agent_analytics|"service agent analytics"|serviceagentanalytics)
|
|
78
|
+
echo "sfdc_internal__Service_Agent_Analytics" ;;
|
|
79
|
+
sfdc_internal__c360unifiedsemanticmodel|c360unifiedsemanticmodel|"c360 unified semantic model"|c360_unified_semantic_model|c360semanticmodel|"c360 semantic model"|c360_semantic_model|c360)
|
|
80
|
+
echo "sfdc_internal__C360UnifiedSemanticModel" ;;
|
|
81
|
+
"agent analytics"|agentanalytics|agent_analytics)
|
|
82
|
+
# Ambiguous — two templates match. Ask the caller which one.
|
|
83
|
+
echo "AMBIGUOUS_AGENT_ANALYTICS" ;;
|
|
84
|
+
0nk*)
|
|
85
|
+
# Raw Salesforce template id (case-insensitive `0Nk…`). We can't resolve
|
|
86
|
+
# to a canonical apiName here without a REST call, so pass through with
|
|
87
|
+
# a marker; step 1's Path B will do the /templates lookup and re-check
|
|
88
|
+
# the resolved .name against the allowlist. Echo the ORIGINAL (case-
|
|
89
|
+
# preserving) raw input, not the lowercased $norm — Salesforce ids are
|
|
90
|
+
# case-sensitive on the wire.
|
|
91
|
+
echo "$raw" ;;
|
|
92
|
+
*)
|
|
93
|
+
echo "REFUSED" ;;
|
|
94
|
+
esac
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
TEMPLATE_NAME_OR_ID=$(resolve_allowed_template "$USER_INPUT")
|
|
98
|
+
|
|
99
|
+
case "$TEMPLATE_NAME_OR_ID" in
|
|
100
|
+
REFUSED)
|
|
101
|
+
cat >&2 <<'MSG'
|
|
102
|
+
Sorry — this skill only installs Service Insights, Sales Insights TN, Employee Agent Analytics, Service Agent Analytics, and C360 Semantic Model.
|
|
103
|
+
|
|
104
|
+
The requested template is not on the allowlist and this skill will not proceed.
|
|
105
|
+
MSG
|
|
106
|
+
return 1 2>/dev/null || exit 1
|
|
107
|
+
;;
|
|
108
|
+
AMBIGUOUS_AGENT_ANALYTICS)
|
|
109
|
+
cat >&2 <<'MSG'
|
|
110
|
+
"Agent Analytics" matches two allowlisted templates:
|
|
111
|
+
- sfdc_internal__Employee_Agent_Analytics (Employee-facing)
|
|
112
|
+
- sfdc_internal__Service_Agent_Analytics (Service-facing)
|
|
113
|
+
Ask the caller which one to install, then re-invoke with the full apiName.
|
|
114
|
+
MSG
|
|
115
|
+
return 1 2>/dev/null || exit 1
|
|
116
|
+
;;
|
|
117
|
+
esac
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The refusal message deliberately DOES NOT list alternatives, name specific non-allowlisted templates, or route the caller to other tooling. That's the guard against turning this skill into an index of "here's what you could have installed instead" — see anti-pattern rule.
|
|
121
|
+
|
|
122
|
+
**Allowlist gate — invariants:**
|
|
123
|
+
- The gate runs BEFORE any App-Framework REST call. Do not `GET /templates` first "just to help the user pick" — this skill never suggests non-allowlisted templates.
|
|
124
|
+
- If the user hands you a raw `0Nk…` id, RESOLVE it against `GET /templates` and reject if the resolved `.name` is not one of the five allowlisted apiNames.
|
|
125
|
+
- If the org's `GET /templates` response omits the allowlisted template the user asked for (feature-gated off, wrong release, etc.), report "not available on this org" and STOP — do NOT offer a "similar" template.
|
|
126
|
+
- The refusal message MUST NOT list alternatives outside the allowlist. Silence on alternatives is deliberate.
|
|
127
|
+
|
|
128
|
+
## When to use this vs alternatives
|
|
129
|
+
|
|
130
|
+
- **Use this skill** whenever the user wants to install **one of the five allowlisted templates** (Service Insights, Sales Insights TN, Employee Agent Analytics, Service Agent Analytics, C360 Semantic Model) on the currently-authenticated org. This is the default path for those five.
|
|
131
|
+
- **Refuse and stop** for any other template request — do not fall through to a "generic" install path. The whole scope of this skill is the allowlist.
|
|
132
|
+
- **Route to `codey-gap-triage`** if the user is describing a missing capability (e.g. "the readiness check doesn't validate chain nodes when readiness.json is absent") rather than an install. Chain-node evaluation is a separate MVP2 concern.
|
|
133
|
+
- **Do NOT delegate to another skill for orchestration** of an allowlisted install — this one is authoritative.
|
|
134
|
+
|
|
135
|
+
## Install-target thumb rule (READ BEFORE ANY `POST /apps`)
|
|
136
|
+
|
|
137
|
+
**The `--target-org` for the caller-template `POST /apps` is ALWAYS `$ORG_ALIAS` (the org the user named), NEVER `$RESOLVED_ORG_FOR_C360`, NEVER `$HOME_ORG_ALIAS`.** The C360-on-HOME auto-install driven by 2b.3/2b.5 is a *prerequisite* that installs a *different template* (`sfdc_internal__C360UnifiedSemanticModel`) on the HOME publisher — it must not be conflated with the caller template's install target. Two cases, no exceptions:
|
|
138
|
+
|
|
139
|
+
1. **`$ORG_ALIAS` is HOME (standalone or paired publisher).** Everything happens in `$ORG_ALIAS`. If 2b.3 detects C360 is missing on this org, `install_c360_on_home_and_wait_for_sync` self-installs C360 here (target = `$ORG_ALIAS` = `$RESOLVED_ORG_FOR_C360`); then the caller template (`SalesInsightsTN`, `ServiceInsights`, `Employee_Agent_Analytics`, `Service_Agent_Analytics`) also installs here in step 4. One org, two POSTs (only if C360 was missing), both targeting `$ORG_ALIAS`.
|
|
140
|
+
|
|
141
|
+
2. **`$ORG_ALIAS` is COMPANION.** The C360 prerequisite installs on `$HOME_ORG_ALIAS` (the paired publisher, target = `$RESOLVED_ORG_FOR_C360` = `$HOME_ORG_ALIAS`); the SDM then propagates from HOME to the companion via multi-org sync (`wait_for_c360_sdm_visible` blocks on this). Only *after* SDM visibility on the companion does step 4 fire, and its target MUST be `$ORG_ALIAS` (the companion). Two orgs, two POSTs against two different `--target-org` values — do not merge them.
|
|
142
|
+
|
|
143
|
+
The caller-template install NEVER lands on `$HOME_ORG_ALIAS` — HOME is the C360 publisher, not a place to host consumer apps on the COMPANION path. If you find yourself typing `--target-org "$HOME_ORG_ALIAS"` in step 4 or later steps, STOP — that's the bug this section exists to prevent. The mandatory guard in step 4 (`Step-4 target-org guard`) refuses at runtime, but you should also refuse at reading time.
|
|
144
|
+
|
|
145
|
+
Consumer templates on the COMPANION path (post-2b, i.e. step 3 readiness through step 5 poll) all use `$ORG_ALIAS`:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# HOME path (standalone or publisher):
|
|
149
|
+
# $ORG_ALIAS == $RESOLVED_ORG_FOR_C360; both C360 (2b.3) and caller template (step 4) install here.
|
|
150
|
+
# COMPANION path:
|
|
151
|
+
# C360 (2b.3): --target-org "$RESOLVED_ORG_FOR_C360" (== $HOME_ORG_ALIAS)
|
|
152
|
+
# Caller template (step 3 readiness, step 4 create, step 5 poll, step 6 delete, step 6b update):
|
|
153
|
+
# --target-org "$ORG_ALIAS" (== the companion)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Preconditions
|
|
157
|
+
|
|
158
|
+
Before running the steps, confirm:
|
|
159
|
+
|
|
160
|
+
1. **Org is authenticated.** Run `sf org list --json` and require at least one org where `isConnected: true`. If the user hasn't told you which alias to use, prefer:
|
|
161
|
+
- the org they named in the last message,
|
|
162
|
+
- otherwise `sf config get target-org --json` (`.result[0].value`),
|
|
163
|
+
- otherwise the first `isConnected: true` entry.
|
|
164
|
+
If nothing is connected, stop and ask them to `sf org login web`.
|
|
165
|
+
|
|
166
|
+
2. **Caller holds the `AppFrameworkManageTemplate` user permission** (the "Manage App Templates" perm). The readiness, create, and delete calls all return HTTP 403 `ACCESS_DENIED` / `INSUFFICIENT_ACCESS` without it. If any App-Framework endpoint returns 403, this is almost always the cause — tell the user to have an admin assign a permission set carrying `AppFrameworkManageTemplate`, then retry. Distinct from feature-gate 403s (see precondition 3).
|
|
167
|
+
|
|
168
|
+
3. **Tableau Next is enabled on the org.** If `GET $API_BASE/templates` returns an empty list, `INSUFFICIENT_ACCESS` on the endpoint itself (not the caller), or `NOT_FOUND`, Tableau Next / App-Framework isn't provisioned — this skill can't fix that; route the user to Tableau Next enablement (out of scope).
|
|
169
|
+
|
|
170
|
+
4. **Data Cloud is provisioned AND the target data space exists.** The readiness/create bodies bind any DataSpaceType variable to an existing data space name; a missing/invalid data space does NOT surface as a clean readiness `FailedStatus` — it fails variable-definition parsing FIRST, so readiness POST returns HTTP 400 `TEMPLATEPARSEERROR: Data space [<X>] ... is unknown` (wire-verified 2026-08-19), or HTTP 400 `CDP not enabled` when Data Cloud is off entirely. **Always run the resolve-data-space pre-flight (`GET /ssot/data-spaces`) before the readiness POST** so an opaque `TEMPLATEPARSEERROR` becomes an actionable "enable Data Cloud / create the data space" message. See wire rule G.
|
|
171
|
+
|
|
172
|
+
5. **User has confirmed which template.** Don't guess. If the user said "install the sales app" but the org has three sales-adjacent templates, ask them to pick.
|
|
173
|
+
|
|
174
|
+
## Discover API version + resolve endpoints
|
|
175
|
+
|
|
176
|
+
Do NOT hardcode `v67.0` — resolve the highest API version the org actually publishes at runtime, and derive every endpoint URL from it. This lets the skill work on any org (test, sandbox, prod) regardless of the release train.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# GET /services/data returns an array of {label, url, version}. Take max(version).
|
|
180
|
+
# `| floor` is REQUIRED: jq's tonumber renders 68.0 as the float 68.0, and the
|
|
181
|
+
# format string "v\(.).0" would then produce "v68.0.0". Coerce to int with floor.
|
|
182
|
+
API_VERSION=$(sf api request rest "/services/data" \
|
|
183
|
+
--target-org "$ORG_ALIAS" --method GET \
|
|
184
|
+
| jq -r '[.[] | .version | tonumber | floor] | max | "v\(.).0"')
|
|
185
|
+
echo "Using API $API_VERSION"
|
|
186
|
+
|
|
187
|
+
# Sanity: expect exactly one dot, e.g. "v68.0" — bail if the format is off.
|
|
188
|
+
case "$API_VERSION" in
|
|
189
|
+
v[0-9]*.0) : ;;
|
|
190
|
+
*) echo "Bad API_VERSION='$API_VERSION' — abort"; return 1 2>/dev/null || exit 1 ;;
|
|
191
|
+
esac
|
|
192
|
+
|
|
193
|
+
# All subsequent App-Framework calls use this base.
|
|
194
|
+
API_BASE="/services/data/$API_VERSION/app-framework"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Every step below references `$API_BASE`. If the discovery call fails (no auth, no `sf` CLI, org unreachable), stop and remediate — every downstream step depends on it.
|
|
198
|
+
|
|
199
|
+
## Wire-shape rules (KNOW THESE COLD — the API is strict)
|
|
200
|
+
|
|
201
|
+
These are the App-Framework Connect API wire shapes as verified on 2026-08-18 against v67.0 (they continue to hold on later versions unless the OAS changes). They are the entire reason the skill exists as a self-contained fallback — the MCP tool wrappers historically drifted from these and the errors are opaque.
|
|
202
|
+
|
|
203
|
+
**A. Readiness body MUST be wrapped as `{"templateValues": {...}}`:**
|
|
204
|
+
- Bare `{}` is rejected with `JSON_PARSER_ERROR: Value does not match expected type`.
|
|
205
|
+
- Empty templateValues (`{"templateValues": {}}`) is allowed — the outer wrapper is what's required.
|
|
206
|
+
- The `required==true` filter is the STARTING point for `templateValues` — but see wire rules E and F for two MUST-DO extensions: unconditional `isTest: false` injection (rule E), and client-side resolution of `{{Variables.X.Y}}` placeholders in defaults (rule F). Step 2's builder handles both automatically. Do not send raw `{{Variables...}}` tokens.
|
|
207
|
+
|
|
208
|
+
**B. Create-app body OAS field is `templateValues` (per `AppInputRepresentation`), NOT `values` and NOT `userInputsForSingleClickDeploy`:**
|
|
209
|
+
- Required top-level fields on the POST body: `templateSourceId`, `label`, `name`.
|
|
210
|
+
- `templateValues` is optional — omit it entirely if step 2 returned no required variables and the user supplied none.
|
|
211
|
+
- Sending `values` or `userInputsForSingleClickDeploy` returns `JSON_PARSER_ERROR: Found unexpected object property`.
|
|
212
|
+
|
|
213
|
+
**C. Data-space variable shape is TEMPLATE-DEPENDENT — dispatch on `variableType.type` from `/configuration`'s `.variables` field:**
|
|
214
|
+
|
|
215
|
+
The variable name is NOT fixed — templates call it `dataSpaceApiName`, `defaultDataspace`, `dataspace`, etc. Do NOT hardcode a variable name. Dispatch on `variableType.type` for every required variable:
|
|
216
|
+
- The wire shape depends on how the template declared the variable in its `variables.json`:
|
|
217
|
+
- `variableType.type == "StringType"` → send as bare String: `"dataSpaceApiName": "default"`.
|
|
218
|
+
- `variableType.type == "DataSpaceType"` → send as an object: `"dataSpaceApiName": {"dataSpaceName": "default"}`.
|
|
219
|
+
- Sending the wrong shape raises symmetric errors:
|
|
220
|
+
- `StringType` template + object → `BAD_REQUEST: variable name [dataSpaceApiName] with value [{dataSpaceName=default}] and type [LinkedHashMap] is not of expected type [String]` (seen on `sfdc_internal__itsm`).
|
|
221
|
+
- `DataSpaceType` template + bare string → `BAD_REQUEST: The variable name [dataSpaceApiName] with value [default] and type [String] is not of expected type [Object]` (seen on `sfdc_internal__servicecloudinsights`).
|
|
222
|
+
- Rule of thumb: NEVER hardcode the shape. Always dispatch on `variableType.type` from step 2's response. Step 2's builder handles this for you.
|
|
223
|
+
|
|
224
|
+
**D. App status field is `applicationStatus`, NOT `status`:**
|
|
225
|
+
- On `GET /app-framework/apps/{id}`, `.status` is null. The real state is `.applicationStatus`.
|
|
226
|
+
- Terminal values: `SuccessStatus` (installed OK), `FailedStatus` (with `.failureReasons[]`). In-flight: `InProgressStatus`.
|
|
227
|
+
|
|
228
|
+
**E. `templateValues` must be complete for ANY variable the template's readiness EL expression references — silently missing variables cause false-negative DMO failures:**
|
|
229
|
+
- Templates like `sfdc_internal__servicecloudinsights` use `${Variables.isTest ? '' : ...}Case__dlm`-style expressions in `DataCloudRowCountReadiness` checks. When `Variables.isTest` isn't bound in `templateValues`, the EL evaluator fails silently, falls back to each check's `valueOnError` (typically `-1`), and the readiness response reports every DMO as "not ready" — even when they exist on the org.
|
|
230
|
+
- The trap: some templates reference `Variables.isTest` in their readiness EL *without declaring `isTest` in their `/configuration` response's `.variables` map*. This means (a) required-only filtering (wire rule A) is not sufficient, and (b) gating on `.variables | has("isTest")` still misses the case. **Always inject `isTest: false` unconditionally** — it's cheap, matches the platform's own test-mode-off convention, and eliminates a whole class of false negatives.
|
|
231
|
+
- Rule of thumb: if readiness comes back with wall-to-wall "DMO not ready" for DMOs the org actually has, suspect an unbound EL var (typically `isTest`) before you suspect the DMOs.
|
|
232
|
+
|
|
233
|
+
**F. `{{Variables.<name>.<subfield>}}` placeholders in variable defaultValues are NOT server-resolved at readiness time — you must inline them client-side:**
|
|
234
|
+
- Some templates ship variable defaults like `CaseContactReasonField.defaultValue = {"objectName": "{{Variables.CaseContactReasonObj.objectName}}", ...}`. The App-Framework `/configuration` endpoint returns the raw placeholder string under `.variables[<name>].defaultValue`; the readiness endpoint does NOT expand it.
|
|
235
|
+
- If you POST the raw placeholder, readiness rejects with e.g. `BAD_REQUEST: Object [{{Variables.CaseContactReasonObj.objectName}}] in variable [CaseContactReasonField] is unknown, not supported, or is invalid.`
|
|
236
|
+
- Resolve placeholders client-side before sending: for each `{{Variables.X.Y}}` token, look up the already-computed value of `X.Y` in `templateValues` and substitute. Iterate to a fixed point (a resolved value may itself reference another placeholder). Step 2's builder does this pass.
|
|
237
|
+
|
|
238
|
+
**G. Data space names must be RESOLVED via `GET /ssot/data-spaces`, never hardcoded as `"default"`:**
|
|
239
|
+
- Any variable of type `DataSpaceType`, or a `StringType` variable whose name semantically carries a data space apiName (e.g. `dataSpaceApiName`, `defaultDataspace`), takes a real data space name — not the literal string `"default"`.
|
|
240
|
+
- Some orgs have no data space named `default`; their sole data space might be `Corp_Ds`, `Sales_Ds`, `Foundations_Corp_Ds`, etc. Wire-verified 2026-08-19: on three such orgs, posting `dataSpaceApiName: {"dataSpaceName": "default"}` returned HTTP 400 `TEMPLATEPARSEERROR: Data space [default] ... is unknown`. On a Data-Cloud-off org it returned HTTP 403 on the underlying `ssot/data-spaces` endpoint or HTTP 400 `CDP not enabled` on the readiness POST.
|
|
241
|
+
- **Do this before step 3 (readiness)**: `DATA_SPACE_NAME=$(sf api request rest "/services/data/$API_VERSION/ssot/data-spaces" --target-org "$ORG_ALIAS" --method GET | jq -r '(.dataSpaces // [] | .[0].name) // "default"')` then substitute `$DATA_SPACE_NAME` into every DataSpaceType / data-space-carrier variable. The pre-flight also catches Data-Cloud-off orgs early (empty list or 403) so you can fail with a clear "enable Data Cloud" message instead of an opaque `TEMPLATEPARSEERROR` on the readiness call.
|
|
242
|
+
|
|
243
|
+
**H. `NumberType` variables MUST be emitted as FLOAT / decimal literals — bare integers 400 on the wire:**
|
|
244
|
+
- The `/configuration` endpoint serializes both `defaultValue` and `variableType.enums` (under `.variables[<name>]`) as BARE INTEGERS (e.g. an Agent Analytics `maxWaitTimeInMinutesForDMO` comes back as `defaultValue: 360` with `enums: [5, 10, 15, 30, 45, 60, 120, 360]` — no decimal point anywhere). You **cannot** infer the required format from the variable definition.
|
|
245
|
+
- The readiness/create validator requires the FLOAT form and rejects the integer. Wire-verified 2026-08-19: POSTing `maxWaitTimeInMinutesForDMO: 360` returns HTTP 400 `The variable name [maxWaitTimeInMinutesForDMO] with value [360] must be one of the following values [5.0, 10.0, 15.0, 30.0, 45.0, 60.0, 120.0, 360.0].`
|
|
246
|
+
- **Rule: append `.0` to every whole-number NumberType value before POSTing** (`360` → `360.0`, `30` → `30.0`). Do NOT "preserve the returned JSON type" for numbers — the returned type is a bare int and is wrong on the wire. This applies to enum-constrained AND free NumberType variables. Non-integer NumberType values (already decimal) pass through unchanged. Keep `DateType` / `DateTimeType` values as their exact returned strings; keep string-typed values as strings.
|
|
247
|
+
|
|
248
|
+
**I. Update-app (`PUT /app-framework/apps/{id}`) — same body shape as create, minus `label`/`name`:**
|
|
249
|
+
- Endpoint: `PUT /services/data/{api_version}/app-framework/apps/{appId}/`. This is the API-driven equivalent of App Hub → tile → **Update** (aka Reconfigure). It re-runs the template's install chain against the existing app record, upgrading it to the template's *current* `templateVersion` and re-applying the supplied `templateValues`. Use this to auto-remediate a 2b.5 C360 version mismatch on the HOME org without asking the user to click through the App Hub wizard.
|
|
250
|
+
- Body:
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"templateSourceId": "sfdc_internal__C360UnifiedSemanticModel",
|
|
254
|
+
"templateValues": {
|
|
255
|
+
"isTest": false,
|
|
256
|
+
"DataSpace": { "dataSpaceName": "default" }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
- `templateSourceId` MUST match the apiName of the template that originally installed the app. On a version drift, the template metadata may have advanced but the apiName is stable — use the apiName the app was installed with (read from `GET /apps/{id}` — either `.templateSourceId` or `.templateApiName`, whichever the release exposes).
|
|
261
|
+
- `templateValues` follows the SAME wire rules as create (A/C/E/F/G/H): required-var filter + `isTest` unconditional inject + `{{Variables.X.Y}}` client-side resolution + DataSpaceType/StringType dispatch + `NumberType` decimal coercion. The step-2 builder is reused verbatim — do NOT hand-craft a different body for PUT.
|
|
262
|
+
- Data space name MUST be resolved via `GET /ssot/data-spaces` (wire rule G). Do not hardcode `"default"` — the readiness stage of the update chain re-validates it and rejects unknown data spaces with the same `TEMPLATEPARSEERROR` as create.
|
|
263
|
+
- Do NOT include `label` or `name` in the PUT body — those are set at create-time and are read-only on update. Sending them either 400s or is silently ignored depending on the release.
|
|
264
|
+
- Response: same async-install pattern as create — the PUT returns quickly with an updated app envelope (`applicationStatus: InProgressStatus` typically) and the caller then polls `GET /apps/{id}` until terminal (wire rule D — check `.applicationStatus`, not `.status`; terminal values are `SuccessStatus`, `SuccessWithWarningsStatus`, `FailedStatus`). Poll cadence and timeout are the same as step 5.
|
|
265
|
+
- Auth / permission: same as create — caller needs `AppFrameworkManageTemplate`. The PUT target org MUST be the org that owns the app record (i.e. HOME on the C360-mismatch remediation path — `$RESOLVED_ORG_FOR_C360`). A companion session cannot PUT against a HOME app because sessions do not cross the Home/Companion boundary. Route the update through the HOME org alias exactly as with the manual Reconfigure.
|
|
266
|
+
- Companion blast radius still applies: the HOME's C360 app is shared by every paired companion. Auto-remediating from ONE companion's caller-invocation would mutate a shared resource for all of them. This is a policy decision, not a wire limitation — 2b.5 CONFIRMS with the user before running PUT on the companion-remediation path (see step 2b.5).
|
|
267
|
+
|
|
268
|
+
## Steps
|
|
269
|
+
|
|
270
|
+
Every step below uses `sf api request rest` — no MCP tool is required. If you prefer to use `mcp__project-codey__*` or `mcp__tableau-next-apps-mcp-beta__*` as an accelerator when they're connected and healthy, see "Fast path (optional)"; the wire-shape rules above still apply.
|
|
271
|
+
|
|
272
|
+
**All snippets below assume `$API_BASE` and `$ORG_ALIAS` are set** — `$API_BASE` comes from the "Discover API version + resolve endpoints" section above; `$ORG_ALIAS` is the sf-CLI alias for the target org. Copying a step in isolation without running discovery first will produce a bare `/app-framework/...` URL and every call will 404.
|
|
273
|
+
|
|
274
|
+
### 0. Preflight generic checks (delegated sub-skills)
|
|
275
|
+
|
|
276
|
+
Before touching templates, run the two **generic, app-agnostic readiness checks** every App-Framework install shares. Each is packaged as its own `user-invocable: false` skill and invoked here — the agent reads the sub-skill file on demand and runs its single read-only probe in this same shell, so an org that isn't provisioned fails fast with a clear message instead of an opaque error many steps later. (The third generic check — the C360 base-model gate — runs at Step 2b, not here, because it needs the *resolved* template's `/assets`.)
|
|
277
|
+
|
|
278
|
+
Run both **before Step 1**, in order, in the current shell.
|
|
279
|
+
|
|
280
|
+
> **Sub-skill path resolution (read first).** The three delegated sub-skills (`tableau-next-readiness`, `tableau-next-datacloud-readiness`, `tableau-next-c360-basemodel-gate`) are installed as sibling folders under `./.afv/skills/`, **relative to your current working directory (the workspace root — the folder that contains `.afv/`)**. Read each with a plain filesystem read at the literal relative path `./.afv/skills/<child>/SKILL.md`. There is **NO** `load_skill` / `search_skills` / `skill_bridge` tool on this host — a filesystem read is the ONLY route. Do **NOT** construct a path from memory, do **NOT** use any absolute path, and do **NOT** use any editor/extension `globalStorage` path — those hold System/Global skills and contain **none** of these workspace sub-skills. If the literal relative read fails, **STOP and report** — never substitute a remembered, absolute, or `globalStorage` location. Secondary fallback ONLY if the `./.afv/skills/` directory does not exist relative to CWD: the HOME-relative `~/.claude/skills/<child>/SKILL.md` (Claude Code). Never use `.vibes`.
|
|
281
|
+
|
|
282
|
+
> **Invoke the Tableau Next readiness check.** Read the literal relative path `./.afv/skills/tableau-next-readiness/SKILL.md`, resolved relative to your current working directory (the workspace root) — do **NOT** build a path from memory, and do **NOT** use any absolute or `globalStorage` path; if this read fails, **STOP and report** (never substitute a remembered path); secondary fallback ONLY if `./.afv/skills/` is absent: the HOME-relative `~/.claude/skills/tableau-next-readiness/SKILL.md` — and execute its check against `$ORG_ALIAS` / `$API_BASE` in the current shell. It confirms `GET $API_BASE/templates` returns a non-empty list (Tableau Next / App-Framework is provisioned, not empty / 403 / NOT_FOUND) and leaves the fetched payload in `$TEMPLATES_JSON`. It returns **PROCEED** or **STOP** (Tableau Next not provisioned — route the user to enablement; out of scope for this skill).
|
|
283
|
+
|
|
284
|
+
> **Invoke the Data Cloud readiness check.** Read the literal relative path `./.afv/skills/tableau-next-datacloud-readiness/SKILL.md`, resolved relative to your current working directory (the workspace root) — do **NOT** build a path from memory, and do **NOT** use any absolute or `globalStorage` path; if this read fails, **STOP and report** (never substitute a remembered path); secondary fallback ONLY if `./.afv/skills/` is absent: the HOME-relative `~/.claude/skills/tableau-next-datacloud-readiness/SKILL.md` — and execute its check against `$ORG_ALIAS` / `$API_VERSION` in the current shell. It confirms Data Cloud is provisioned and resolves the target data space into `$DATA_SPACE_NAME` (wire rule G — never hardcode `"default"`). It returns **PROCEED** or **STOP** (Data Cloud off / no data space — an install that binds a `DataSpaceType` variable would otherwise 400 with `TEMPLATEPARSEERROR`).
|
|
285
|
+
|
|
286
|
+
**Hard gate — do NOT continue to Step 1 unless BOTH checks returned PROCEED.** If either STOPs, surface its message verbatim and halt.
|
|
287
|
+
|
|
288
|
+
**Outputs these checks leave in your shell (later steps reuse them — run them in the SAME shell as the rest of this skill):** `$TEMPLATES_JSON` (Step 1 reuses it instead of re-fetching) and `$DATA_SPACE_NAME` (Step 2's builder and wire rule G reuse it instead of re-resolving). If your harness ran a check in a separate shell, those variables won't be set here — the corresponding step re-derives them idempotently via a `${VAR:-…}` fallback, so the skill still works, just with one extra GET.
|
|
289
|
+
|
|
290
|
+
### 1. Enumerate templates (filtered by allowlist)
|
|
291
|
+
|
|
292
|
+
The allowlist gate above has already produced a canonical apiName in `$TEMPLATE_NAME_OR_ID`. This step confirms the template EXISTS on this org and captures its `.id` for downstream logging. It also serves as the check that catches the "user handed us a raw `0Nk…` id" path — we look the id up and reject if its `.name` isn't on the allowlist.
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
ALLOWLIST='["sfdc_internal__ServiceInsights","sfdc_internal__SalesInsightsTN","sfdc_internal__Employee_Agent_Analytics","sfdc_internal__Service_Agent_Analytics","sfdc_internal__C360UnifiedSemanticModel"]'
|
|
296
|
+
|
|
297
|
+
# Reuse the payload the preflight 'tableau-next-readiness' check already fetched
|
|
298
|
+
# (Step 0); re-fetch only if this step is run standalone (TEMPLATES_JSON unset).
|
|
299
|
+
TEMPLATES_JSON="${TEMPLATES_JSON:-$(sf api request rest "$API_BASE/templates" \
|
|
300
|
+
--target-org "$ORG_ALIAS" --method GET)}"
|
|
301
|
+
|
|
302
|
+
# Path A — canonical apiName input (either resolve_allowed_template produced it or
|
|
303
|
+
# the caller supplied it directly). Path B — raw `0Nk…` id.
|
|
304
|
+
# We must re-check the allowlist on BOTH paths — Path A because $TEMPLATE_NAME_OR_ID
|
|
305
|
+
# may have been set outside resolve_allowed_template (env var, prior shell state,
|
|
306
|
+
# hand-editing), and Path B because ids don't self-identify their template name.
|
|
307
|
+
# This is a DOUBLE GATE: resolve_allowed_template (up-front) + this step (post-
|
|
308
|
+
# resolve). Do NOT rely on either alone.
|
|
309
|
+
# case-glob (dash/sh/bash portable) — the rest of the file uses `case`, not `[[ ... ]]`.
|
|
310
|
+
case "$TEMPLATE_NAME_OR_ID" in
|
|
311
|
+
0Nk*)
|
|
312
|
+
# Path B: id → resolve .name via /templates.
|
|
313
|
+
RESOLVED_NAME=$(echo "$TEMPLATES_JSON" | jq -r \
|
|
314
|
+
--arg id "$TEMPLATE_NAME_OR_ID" \
|
|
315
|
+
'.templates[] | select(.id == $id) | .name' | head -n 1)
|
|
316
|
+
if [ -z "$RESOLVED_NAME" ]; then
|
|
317
|
+
echo "Template id $TEMPLATE_NAME_OR_ID is not published on this org. Refusing." >&2
|
|
318
|
+
return 1 2>/dev/null || exit 1
|
|
319
|
+
fi
|
|
320
|
+
# Rewrite to canonical apiName for the rest of the flow — the allowlist check below
|
|
321
|
+
# then runs on the CANONICAL name for both paths uniformly.
|
|
322
|
+
TEMPLATE_NAME_OR_ID="$RESOLVED_NAME"
|
|
323
|
+
;;
|
|
324
|
+
esac
|
|
325
|
+
|
|
326
|
+
# Belt-and-braces allowlist re-check — applies to Path A AND (post-resolve) Path B.
|
|
327
|
+
# This closes the "env-var bypass" escape hatch: even if $TEMPLATE_NAME_OR_ID was
|
|
328
|
+
# populated without going through resolve_allowed_template, a non-allowlisted
|
|
329
|
+
# apiName is rejected HERE before any variables/readiness/create call runs.
|
|
330
|
+
IS_ALLOWED=$(echo "$ALLOWLIST" | jq -r --arg n "$TEMPLATE_NAME_OR_ID" 'index($n) != null')
|
|
331
|
+
if [ "$IS_ALLOWED" != "true" ]; then
|
|
332
|
+
echo "Template '$TEMPLATE_NAME_OR_ID' is not on the allowlist. Refusing." >&2
|
|
333
|
+
return 1 2>/dev/null || exit 1
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
# Show ONLY the allowlisted templates that are present on this org.
|
|
337
|
+
echo "$TEMPLATES_JSON" | jq --argjson allow "$ALLOWLIST" '
|
|
338
|
+
.templates[]
|
|
339
|
+
| select(.name as $n | $allow | index($n))
|
|
340
|
+
| {name, label, id, templateType, templateSubtype, templateStatus}
|
|
341
|
+
'
|
|
342
|
+
|
|
343
|
+
# Confirm the chosen template is actually present on this org before proceeding.
|
|
344
|
+
EXISTS=$(echo "$TEMPLATES_JSON" | jq -r --arg n "$TEMPLATE_NAME_OR_ID" \
|
|
345
|
+
'.templates | map(select(.name == $n)) | length')
|
|
346
|
+
if [ "$EXISTS" = "0" ]; then
|
|
347
|
+
echo "Template $TEMPLATE_NAME_OR_ID is not available on this org (not returned by /templates)." >&2
|
|
348
|
+
echo "This is a scope / provisioning issue. This skill will NOT suggest a substitute." >&2
|
|
349
|
+
return 1 2>/dev/null || exit 1
|
|
350
|
+
fi
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Response follows `TemplateCollectionRepresentation`: entries under `.templates[]`, each is a `TemplateRepresentation` with `.id`, `.name` (API name), `.label`, `.description`, `.templateType`, `.templateSubtype`, `.templateStatus`.
|
|
354
|
+
|
|
355
|
+
Present a short list to the user — `name` + `label` — showing ONLY the allowlisted templates present on this org, and ask which one to install. Never install without an explicit user pick. Never expand the list beyond the allowlist.
|
|
356
|
+
|
|
357
|
+
**Failure — empty `.templates[]`:** Tableau Next isn't enabled OR user lacks visibility. Say so; don't retry.
|
|
358
|
+
**Failure — the allowlisted template the user chose is not in `.templates[]`:** the template isn't provisioned on this org. Report "not available on this org" and stop — do NOT suggest an alternative (see allowlist invariants).
|
|
359
|
+
|
|
360
|
+
### 2. Fetch the template's required variables
|
|
361
|
+
|
|
362
|
+
Use `GET /app-framework/templates/{id}/configuration` — the variables live under `.variables` at the top of the response body. This endpoint replaces the older `/variables` endpoint (which no longer ships on newer API versions); `/configuration` returns the full install-time configuration blob with the same `.variables` OBJECT MAP wire shape underneath.
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
sf api request rest "$API_BASE/templates/$TEMPLATE_NAME_OR_ID/configuration" \
|
|
366
|
+
--target-org "$ORG_ALIAS" --method GET \
|
|
367
|
+
| jq '.variables'
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Response shape — `.variables` is an OBJECT MAP, not an array.** The `/configuration` response wraps it at the top level. Real templates (e.g. `sfdc_internal__itsm`) return:
|
|
371
|
+
|
|
372
|
+
```json
|
|
373
|
+
{
|
|
374
|
+
"variables": {
|
|
375
|
+
"VarNameA": { "defaultValue": "...", "required": true, "variableType": "StringType", ... },
|
|
376
|
+
"VarNameB": { "defaultValue": "...", "required": false, "variableType": "StringType", ... }
|
|
377
|
+
},
|
|
378
|
+
...other configuration keys...
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Downstream jq / python passes below reference `.variables` off the top-level `/configuration` payload exactly as before — the wrapper key is unchanged, only the HTTP path moved.
|
|
383
|
+
|
|
384
|
+
Iterate it with `.variables | to_entries[]` — writing `.variables[]` or `.variables[]?` will yield the *value objects* (whose `.name` is undefined) and any subsequent `{(.name): ...}` construction crashes with `Cannot use null (null) as object key`.
|
|
385
|
+
|
|
386
|
+
**Only variables where `required == true` belong in `templateValues`** — WITH TWO EXCEPTIONS documented below. The server does NOT support `?required=true` as a query param — filter client-side in jq. Sending optional variables is harmless most of the time but muddies the payload and can trip stricter server-side validation for readiness on some templates.
|
|
387
|
+
|
|
388
|
+
**Exception 1 — sentinel defaults for required vars with `defaultValue == null`.** Some templates declare a required variable with no default (e.g. `dataSpaceApiName` on `sfdc_internal__servicecloudinsights` is required + `DataSpaceType` + `defaultValue: null`). If you simply skip null defaults, the readiness call fails with `variable [X] is required`. Inject a type-aware sentinel:
|
|
389
|
+
|
|
390
|
+
| `variableType.type` | Sentinel value | Rationale |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| `DataSpaceType` | `{"dataSpaceName": "<data_space_name>"}` | wire rule C object form. **Never hardcode `"default"`** — resolve the actual data space name from `GET /ssot/data-spaces` first (see wire rule G below). The rest of the codepath validated this on 2026-08-19: three orgs returned a `TEMPLATEPARSEERROR: Data space [default] ... unknown` on the readiness POST because their data spaces have non-`default` apiNames. |
|
|
393
|
+
| `StringType` + variable name that semantically carries a data space apiName (e.g. `defaultDataspace`, `dataSpaceApiName`, `data_space_name`, `dataspaceId`) | `"<data_space_name>"` | Same — the ACTUAL apiName resolved via `GET /ssot/data-spaces`, not the literal string `"default"`. Empty string fails the server-side DataSpace apiName lookup; the wrong string 400s with `TEMPLATEPARSEERROR: Data space [X] ... unknown`. |
|
|
394
|
+
| `StringType` (any other name) | `""` | Empty string is the platform's convention for "not specified" on plain string inputs. |
|
|
395
|
+
| `BooleanType` | `false` | Matches the platform's default-off convention. |
|
|
396
|
+
| `NumberType` | (see wire rule G below — always emit as a decimal literal) | Bare integers 400 on the wire. |
|
|
397
|
+
| any other type | (log a warning + skip) | Unknown types must be resolved by the caller — never invent. |
|
|
398
|
+
|
|
399
|
+
**Exception 2 — always inject `isTest: false`, unconditionally.** This is wire rule E: readiness EL expressions can reference `Variables.isTest` in DMO-name construction, and some templates (e.g. `sfdc_internal__servicecloudinsights`) reference it in EL *without declaring it in `/configuration`'s `.variables`*. Leaving it unbound silently returns `-1` for every DMO check. Gating on "template declared isTest" misses the pathological case, so just always inject `false` — it's a no-op for templates that don't consume it.
|
|
400
|
+
|
|
401
|
+
Build the `templateValues` map used in steps 3 and 4 — the jq below does the required filter, sentinel injection, `isTest` sentinel, AND the `{{Variables.X.Y}}` placeholder resolution (wire rule F) in one pass:
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
# Fetch the template configuration once. Variables live under `.variables`
|
|
405
|
+
# (an object map) — we keep the whole /configuration payload in VARS_JSON so
|
|
406
|
+
# every downstream `.variables | to_entries[]` walk below continues to work
|
|
407
|
+
# without a change.
|
|
408
|
+
VARS_JSON=$(sf api request rest "$API_BASE/templates/$TEMPLATE_NAME_OR_ID/configuration" \
|
|
409
|
+
--target-org "$ORG_ALIAS" --method GET)
|
|
410
|
+
|
|
411
|
+
# ---- Pass 1: build the raw templateValues map -------------------------------
|
|
412
|
+
# - iterate .variables as key/value pairs (it's an object map)
|
|
413
|
+
# - keep required == true OR name == "isTest" (wire rule E — readiness needs it bound)
|
|
414
|
+
# - if defaultValue is null AND variable is required, inject a type-aware sentinel
|
|
415
|
+
# (wire rule C: DataSpaceType → object form; StringType → ""; BooleanType → false)
|
|
416
|
+
# Exception: StringType variables that semantically CARRY a data space name
|
|
417
|
+
# (variable name contains "datasp" case-insensitive — matches `dataSpaceApiName`,
|
|
418
|
+
# `defaultDataspace`, `dataspace`, `dataSpace`, etc.) sentinel to the ACTUAL
|
|
419
|
+
# resolved data space name (see wire rule G — resolve-data-space pre-flight),
|
|
420
|
+
# NOT the literal string "default". Some orgs have no data space named "default"
|
|
421
|
+
# (their sole data space might be `Corp_Ds`, `Sales_Ds`, etc.), and posting
|
|
422
|
+
# "default" 400s with `TEMPLATEPARSEERROR: Data space [default] ... unknown`.
|
|
423
|
+
# The caller MUST resolve $DATA_SPACE_NAME via `GET /ssot/data-spaces` before
|
|
424
|
+
# this pass and substitute it here. Empty string is also wrong — it fails
|
|
425
|
+
# DataSpace lookup with `INVALID_INPUT: DataSpace with apiName [] not found`.
|
|
426
|
+
# - non-null defaults pass through unchanged
|
|
427
|
+
# - `has("defaultValue")` guards `defaultValue // <sentinel>`: raw `//` treats
|
|
428
|
+
# an explicit `false` as null and would clobber a legitimate BooleanType default.
|
|
429
|
+
# Resolve $DATA_SPACE_NAME via GET /ssot/data-spaces FIRST (wire rule G below).
|
|
430
|
+
# Do NOT hardcode "default" — some orgs have no data space by that name; the
|
|
431
|
+
# readiness POST 400s with TEMPLATEPARSEERROR when the string doesn't match.
|
|
432
|
+
# Reuse the value the preflight 'tableau-next-datacloud-readiness' check resolved
|
|
433
|
+
# (Step 0); re-resolve only if this step is run standalone (DATA_SPACE_NAME unset).
|
|
434
|
+
DATA_SPACE_NAME="${DATA_SPACE_NAME:-$(sf api request rest "/services/data/$API_VERSION/ssot/data-spaces" \
|
|
435
|
+
--target-org "$ORG_ALIAS" --method GET \
|
|
436
|
+
| jq -r '(.dataSpaces // [] | .[0].name) // "default"')}"
|
|
437
|
+
|
|
438
|
+
TEMPLATE_VALUES_RAW=$(echo "$VARS_JSON" | jq --arg ds "$DATA_SPACE_NAME" '
|
|
439
|
+
# Sentinel dispatch is TWO-DIMENSIONAL: (variable name pattern, variableType.type).
|
|
440
|
+
# StringType data-space carriers ("defaultDataspace", "dataSpaceApiName",
|
|
441
|
+
# "data_space_name", "DATA_SPACE_NAME", etc.) sentinel to $ds — the LIVE-resolved
|
|
442
|
+
# data space apiName from GET /ssot/data-spaces, NOT the literal "default".
|
|
443
|
+
# Plain-StringType inputs (labels, filter criteria, etc.) sentinel to "" as before.
|
|
444
|
+
#
|
|
445
|
+
# Heuristic: normalize the name (lowercase, strip [_-.]), then require the
|
|
446
|
+
# token "dataspace" as a whole word AT the head OR AT the tail of the string,
|
|
447
|
+
# not just anywhere in it. This catches: dataSpaceApiName, defaultDataspace,
|
|
448
|
+
# data_space, DATA_SPACE_NAME, my.dataspace, dataspaceId — while rejecting
|
|
449
|
+
# accidental substrings like "metadataspec" or "dataSparkline" that happen to
|
|
450
|
+
# contain the letters d-a-t-a-s-p in sequence.
|
|
451
|
+
def is_dataspace_carrier(name):
|
|
452
|
+
(name | ascii_downcase | gsub("[_.\\-]"; ""))
|
|
453
|
+
| test("(^dataspace|dataspace$|^dataspacename$|dataspaceapiname$|dataspaceid$)");
|
|
454
|
+
|
|
455
|
+
def sentinel(name; t):
|
|
456
|
+
if t == "DataSpaceType" then {dataSpaceName: $ds}
|
|
457
|
+
elif t == "StringType" and is_dataspace_carrier(name) then $ds
|
|
458
|
+
elif t == "StringType" then ""
|
|
459
|
+
elif t == "BooleanType" then false
|
|
460
|
+
elif t == "NumberType" then 0.0
|
|
461
|
+
else null end;
|
|
462
|
+
|
|
463
|
+
[ (.variables // {})
|
|
464
|
+
| to_entries[]
|
|
465
|
+
| select(.value.required == true or .key == "isTest")
|
|
466
|
+
| . as $e
|
|
467
|
+
| ( if ($e.value | has("defaultValue")) and ($e.value.defaultValue != null)
|
|
468
|
+
then $e.value.defaultValue
|
|
469
|
+
elif $e.value.required == true
|
|
470
|
+
then sentinel($e.key; ($e.value.variableType.type // $e.value.variableType))
|
|
471
|
+
elif $e.key == "isTest"
|
|
472
|
+
then false
|
|
473
|
+
else null end
|
|
474
|
+
) as $v
|
|
475
|
+
| select($v != null)
|
|
476
|
+
| {($e.key): $v}
|
|
477
|
+
] | add // {}
|
|
478
|
+
')
|
|
479
|
+
|
|
480
|
+
# Warn on any required var whose variableType.type has no sentinel — the row was
|
|
481
|
+
# silently dropped above. Surface to stderr so the caller can supply a value.
|
|
482
|
+
UNKNOWN_TYPES=$(echo "$VARS_JSON" | jq -r '
|
|
483
|
+
(.variables // {})
|
|
484
|
+
| to_entries[]
|
|
485
|
+
| select(.value.required == true)
|
|
486
|
+
| select(((.value | has("defaultValue")) | not) or (.value.defaultValue == null))
|
|
487
|
+
| . as $e
|
|
488
|
+
| ($e.value.variableType.type // $e.value.variableType) as $t
|
|
489
|
+
| select($t != null)
|
|
490
|
+
| select(["DataSpaceType","StringType","BooleanType","NumberType"] | index($t) | not)
|
|
491
|
+
| "\($e.key) (variableType.type=\($t))"
|
|
492
|
+
')
|
|
493
|
+
if [ -n "$UNKNOWN_TYPES" ]; then
|
|
494
|
+
echo "WARN: required variables with null default AND unrecognized variableType — silently skipped, caller must supply values:" >&2
|
|
495
|
+
echo "$UNKNOWN_TYPES" | sed 's/^/ - /' >&2
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
# Belt-and-braces: unconditionally inject `isTest: false` if not already present.
|
|
499
|
+
# Wire rule E — some templates (e.g. sfdc_internal__servicecloudinsights) reference
|
|
500
|
+
# `Variables.isTest` in their readiness EL expressions WITHOUT declaring `isTest`
|
|
501
|
+
# in their /configuration response's `.variables` map. Gating on `.variables | has("isTest")` misses these
|
|
502
|
+
# and causes the wall-of-DMO-not-ready false negative. Always inject.
|
|
503
|
+
TEMPLATE_VALUES_RAW=$(echo "$TEMPLATE_VALUES_RAW" | jq '. + (if has("isTest") then {} else {isTest: false} end)')
|
|
504
|
+
|
|
505
|
+
# ---- Pass 1b: coerce every NumberType value to a decimal literal (wire rule H) --
|
|
506
|
+
# jq collapses `360.0` back to `360` on JSON re-serialization in most versions —
|
|
507
|
+
# so we use Python for this pass. Load the two inputs, walk templateValues, and
|
|
508
|
+
# convert any integer whose key was declared NumberType in /configuration's `.variables` into a
|
|
509
|
+
# float. `json.dumps` in Python DOES preserve the "360.0" form for float values.
|
|
510
|
+
TEMPLATE_VALUES_RAW=$(python3 -c "
|
|
511
|
+
import json, sys
|
|
512
|
+
tv = json.loads(sys.argv[1])
|
|
513
|
+
vs = json.loads(sys.argv[2])
|
|
514
|
+
num_keys = {
|
|
515
|
+
k for k, v in (vs.get('variables') or {}).items()
|
|
516
|
+
if (v.get('variableType') or {}).get('type') == 'NumberType'
|
|
517
|
+
or v.get('variableType') == 'NumberType'
|
|
518
|
+
}
|
|
519
|
+
for k in list(tv):
|
|
520
|
+
if k in num_keys and isinstance(tv[k], int) and not isinstance(tv[k], bool):
|
|
521
|
+
tv[k] = float(tv[k])
|
|
522
|
+
print(json.dumps(tv))
|
|
523
|
+
" "$TEMPLATE_VALUES_RAW" "$VARS_JSON")
|
|
524
|
+
|
|
525
|
+
# ---- Pass 2: resolve {{Variables.X.Y}} placeholders client-side (wire rule F) --
|
|
526
|
+
# Some templates ship defaults like {"objectName": "{{Variables.CaseContactReasonObj.objectName}}"}.
|
|
527
|
+
# The readiness endpoint does NOT expand these — you must inline the referenced value
|
|
528
|
+
# before POSTing. Walk the map recursively, substitute every {{Variables.X.Y[...]}}
|
|
529
|
+
# token with the resolved value from the same map, and iterate until fixed point
|
|
530
|
+
# (a resolved value may itself contain another placeholder).
|
|
531
|
+
resolve_placeholders() {
|
|
532
|
+
local input="$1"
|
|
533
|
+
local prev=""
|
|
534
|
+
local iters=0
|
|
535
|
+
# Cap iterations to catch pathological cycles — 8 passes is plenty in practice.
|
|
536
|
+
while [ "$input" != "$prev" ] && [ $iters -lt 8 ]; do
|
|
537
|
+
prev="$input"
|
|
538
|
+
input=$(echo "$input" | jq --argjson root "$input" '
|
|
539
|
+
# Resolve {{Variables.X.Y.Z...}} tokens against the root map.
|
|
540
|
+
# Contract: strings without a placeholder pass through UNCHANGED. If the
|
|
541
|
+
# ENTIRE string is a single placeholder AND the resolved value is a non-
|
|
542
|
+
# string (object/array/number/bool), return that value verbatim to preserve
|
|
543
|
+
# structure. If the string is a template mixing placeholder(s) with text,
|
|
544
|
+
# each token is stringified and substituted in place (all occurrences).
|
|
545
|
+
#
|
|
546
|
+
# Notes on prior failure modes:
|
|
547
|
+
# - `capture(...)?` returns jq-empty on no-match; combined with
|
|
548
|
+
# `with_entries(.value |= walk_resolve(root))` it nulled every non-
|
|
549
|
+
# placeholder string. Gate with `test(...)` first.
|
|
550
|
+
# - Anchored `capture` only handles ONE placeholder per pass. `gsub`
|
|
551
|
+
# with a named-capture regex handles all occurrences.
|
|
552
|
+
# - `reduce .[$k]` crashes with "Cannot index string" if an intermediate
|
|
553
|
+
# path segment is a scalar. Guard the step.
|
|
554
|
+
def lookup_path(root; path_str):
|
|
555
|
+
(path_str | split(".")) as $keys
|
|
556
|
+
| reduce $keys[] as $k (root;
|
|
557
|
+
if . == null or (type != "object") then null
|
|
558
|
+
else .[$k] end);
|
|
559
|
+
|
|
560
|
+
def resolve_str(s; root):
|
|
561
|
+
if (s | test("\\{\\{Variables\\.")) | not then s
|
|
562
|
+
else
|
|
563
|
+
# Whole-string single-placeholder fast path: return the raw resolved
|
|
564
|
+
# value so we can substitute an object/number/array without stringifying.
|
|
565
|
+
# Note: `capture(...)?` on no-match yields the empty stream (NOT null),
|
|
566
|
+
# which combined with `with_entries(.value |= walk_resolve(root))` would
|
|
567
|
+
# null the surrounding entry. `// null` collapses empty to a concrete null.
|
|
568
|
+
( ((s | capture("^\\{\\{Variables\\.(?<path>[A-Za-z_][A-Za-z0-9_.]*)\\}\\}$")?) // null) ) as $whole
|
|
569
|
+
| if $whole != null then
|
|
570
|
+
(lookup_path(root; $whole.path)) as $v
|
|
571
|
+
| if $v == null then s else $v end
|
|
572
|
+
else
|
|
573
|
+
# Template-substitution path: replace every occurrence in place.
|
|
574
|
+
# Unresolved tokens are left untouched so the outer loop can retry
|
|
575
|
+
# (or the final WARN can surface them).
|
|
576
|
+
(s | gsub("\\{\\{Variables\\.(?<path>[A-Za-z_][A-Za-z0-9_.]*)\\}\\}";
|
|
577
|
+
(lookup_path(root; .path)) as $v
|
|
578
|
+
| if $v == null then "{{Variables.\(.path)}}"
|
|
579
|
+
elif ($v|type) == "string" then $v
|
|
580
|
+
else ($v|tostring) end))
|
|
581
|
+
end
|
|
582
|
+
end;
|
|
583
|
+
|
|
584
|
+
def walk_resolve(root):
|
|
585
|
+
if type == "string" then resolve_str(.; root)
|
|
586
|
+
elif type == "object" then with_entries(.value |= walk_resolve(root))
|
|
587
|
+
elif type == "array" then map(walk_resolve(root))
|
|
588
|
+
else . end;
|
|
589
|
+
|
|
590
|
+
walk_resolve($root)
|
|
591
|
+
')
|
|
592
|
+
iters=$((iters + 1))
|
|
593
|
+
done
|
|
594
|
+
# If unresolved placeholders remain, warn — do not silently POST them.
|
|
595
|
+
if echo "$input" | grep -q '{{Variables\.'; then
|
|
596
|
+
echo "WARN: unresolved {{Variables.X.Y}} placeholders remain in templateValues after $iters passes." >&2
|
|
597
|
+
echo "$input" | jq -r '..
|
|
598
|
+
| strings
|
|
599
|
+
| select(test("\\{\\{Variables\\."))' >&2
|
|
600
|
+
fi
|
|
601
|
+
echo "$input"
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
TEMPLATE_VALUES=$(resolve_placeholders "$TEMPLATE_VALUES_RAW")
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**If the response is empty (`.variables: {}`, or the key absent):** the template is a single-click install. `TEMPLATE_VALUES` will be `{}` — step 3 still needs the outer wrapper (wire rule A), and step 4 can either send `"templateValues": {}` or omit the field entirely. Both are accepted by the server.
|
|
608
|
+
|
|
609
|
+
**If the response returns `NOT_FOUND`:** the templateIdOrApiName doesn't resolve on this org. Re-check spelling and offer to re-list.
|
|
610
|
+
|
|
611
|
+
### 2b. Detect C360 Semantic Model base-model dependency (REQUIRED — do NOT skip)
|
|
612
|
+
|
|
613
|
+
Some allowlisted templates ship SDMs that **extend** the shared `C360SemanticModel_c360` semantic model (a.k.a. `sfdc_internal__C360UnifiedSemanticModel`). If the C360 app isn't already installed on the org — OR is installed at a stale `templateVersion` — the extending SDMs cannot resolve their base model and the install will fail late, after `POST /apps` has already spent minutes provisioning downstream assets. Detect this dependency BEFORE readiness and refuse cleanly.
|
|
614
|
+
|
|
615
|
+
**This check is a generic, app-agnostic prerequisite and is packaged as its own skill: [`tableau-next-c360-basemodel-gate`](../tableau-next-c360-basemodel-gate/SKILL.md).** Every allowlisted install except C360 itself must clear it. Run it now — **before Step 3 (readiness) and before Step 4 (create)** — in this same shell session:
|
|
616
|
+
|
|
617
|
+
> **Invoke the gate.** Read the literal relative path `./.afv/skills/tableau-next-c360-basemodel-gate/SKILL.md`, resolved relative to your current working directory (the workspace root) — do **NOT** build a path from memory, and do **NOT** use any absolute or `globalStorage` path; if this read fails, **STOP and report** (never substitute a remembered path); secondary fallback ONLY if `./.afv/skills/` is absent: the HOME-relative `~/.claude/skills/tableau-next-c360-basemodel-gate/SKILL.md` — and execute its steps against `$ORG_ALIAS` in the current shell. It resolves the HOME/COMPANION multi-org role, detects the C360 base-model dependency in the caller template's `/assets`, and enforces the mandatory 3-step verification (SDM presence → installed C360 app `SuccessStatus` → templateVersion parity). It returns **PROCEED** (all clear) or **REFUSE / STOP** with an actionable message.
|
|
618
|
+
|
|
619
|
+
**Hard gate — do NOT continue past this point unless the gate returned PROCEED.** If it REFUSES or STOPs, surface its message verbatim and halt; do not run Step 3.
|
|
620
|
+
|
|
621
|
+
**Non-bypassable invariants (unchanged — enforced inside the gate skill):**
|
|
622
|
+
- **Skip iff installing C360 itself.** When `$TEMPLATE_NAME_OR_ID == "sfdc_internal__C360UnifiedSemanticModel"` the gate is a no-op — a template does not depend on itself.
|
|
623
|
+
- **Whitelist-green only.** Only `SuccessStatus` / `SuccessWithWarningsStatus` count as an installed C360 app; `InProgressStatus` / `FailedStatus` never seed the installed version.
|
|
624
|
+
- **HOME vs COMPANION asymmetry.** HOME auto-remediates a missing/stale C360 inline (self-install + `update_app`); COMPANION refuses a shared-HOME mutation by default and only proceeds on explicit opt-in (`TABNEXT_ALLOW_SHARED_HOME_C360_UPDATE=1` or a TTY `y`).
|
|
625
|
+
- **templateVersion parity = HARD STOP.** An unreadable installed version OR an unreadable current template version refuses — there is no soft-degrade path.
|
|
626
|
+
|
|
627
|
+
**Outputs the gate leaves in your shell (Steps 4 and 6b depend on these — run the gate in the SAME shell as the rest of this skill):** `$ORG_ROLE` (HOME / COMPANION / UNKNOWN), `$RESOLVED_ORG_FOR_C360` (= `$ORG_ALIAS` on HOME, = `$HOME_ORG_ALIAS` on COMPANION), and `$HOME_ORG_ALIAS`. The Step-4 target-org guard and the Step-6b update path read these directly. If your harness ran the gate in a separate shell, re-run its `resolve_c360_gate_orgs` in this shell before Step 4 so these variables are set.
|
|
628
|
+
|
|
629
|
+
> **Cross-reference note.** Other steps in this skill mention the sub-step numbers **2b.0 / 2b.2 / 2b.3 / 2b.4 / 2b.5** and the helper functions **`resolve_c360_gate_orgs`**, **`wait_for_c360_sdm_visible`**, **`install_c360_on_home_and_wait_for_sync`**, and **`compare_versions`** — these now live inside the `tableau-next-c360-basemodel-gate` skill (the numbering and function names are preserved verbatim there). Those mentions are explanatory; the code that runs them executes inside the gate.
|
|
630
|
+
|
|
631
|
+
### 3. Check readiness
|
|
632
|
+
|
|
633
|
+
**Body construction — wire rule A applies:**
|
|
634
|
+
|
|
635
|
+
```bash
|
|
636
|
+
# Guard: TEMPLATE_VALUES must be valid JSON before we splice it. If step 2 was
|
|
637
|
+
# skipped or produced nothing, default to {} — the outer wrapper is still required.
|
|
638
|
+
[ -z "$TEMPLATE_VALUES" ] && TEMPLATE_VALUES='{}'
|
|
639
|
+
|
|
640
|
+
# Write the readiness body to a temp file so `--body '@file'` parses it directly.
|
|
641
|
+
# `mktemp` without -t is portable on macOS + Linux; the trailing suffix requires GNU
|
|
642
|
+
# `mktemp --suffix=.json`, which isn't universal, so we rename after.
|
|
643
|
+
READINESS_BODY_TMP=$(mktemp /tmp/readiness_body.XXXXXX)
|
|
644
|
+
READINESS_BODY_FILE="${READINESS_BODY_TMP}.json"
|
|
645
|
+
mv "$READINESS_BODY_TMP" "$READINESS_BODY_FILE"
|
|
646
|
+
|
|
647
|
+
# Compose the body via jq so we never emit malformed JSON when TEMPLATE_VALUES is {}.
|
|
648
|
+
echo "$TEMPLATE_VALUES" | jq '{templateValues: .}' > "$READINESS_BODY_FILE"
|
|
649
|
+
|
|
650
|
+
sf api request rest "$API_BASE/templates/$TEMPLATE_NAME_OR_ID/readiness" \
|
|
651
|
+
--target-org "$ORG_ALIAS" --method POST \
|
|
652
|
+
--body "@$READINESS_BODY_FILE" \
|
|
653
|
+
--header "Content-Type: application/json" \
|
|
654
|
+
| jq '.'
|
|
655
|
+
|
|
656
|
+
rm -f "$READINESS_BODY_FILE"
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Inspect the response's status field. Not every template ships a `readiness.json` — for templates without one, the platform's `OrgReadinessAssembler.assemble()` catches `AppFrameworkErrorCode.ChainNotFound` and returns `SuccessWithWarningsStatus`, which reads as green but means "we didn't actually check anything." **Treat `SuccessWithWarnings` as a soft-yes, not a hard-yes.**
|
|
660
|
+
|
|
661
|
+
**Failure modes:**
|
|
662
|
+
- `Status: Failed` in the readiness payload — surface the failure reasons verbatim (they name the DMO, permission, or feature flag that's missing). Do NOT proceed to install.
|
|
663
|
+
- `Status: Success` or `SuccessWithWarnings` — safe to proceed.
|
|
664
|
+
- `JSON_PARSER_ERROR: Value does not match expected type` — your body wasn't wrapped in `templateValues`. Re-read wire rule A.
|
|
665
|
+
- `BAD_REQUEST: variable name [dataSpaceApiName] ... not of expected type [String]` — the template declares `dataSpaceApiName` as `StringType` but you sent the object form. Wire rule C — dispatch on `variableType.type`.
|
|
666
|
+
- `BAD_REQUEST: The variable name [dataSpaceApiName] with value [default] and type [String] is not of expected type [Object]` — the template declares `dataSpaceApiName` as `DataSpaceType` but you sent a bare string. Wire rule C — dispatch on `variableType.type`.
|
|
667
|
+
- `BAD_REQUEST: Object [{{Variables.X.Y}}] in variable [Z] is unknown, not supported, or is invalid` — a variable default contains a `{{Variables.X.Y}}` placeholder that wasn't resolved client-side before POSTing. Wire rule F — step 2's builder resolves these; if you constructed `templateValues` by hand, run the placeholder-resolution pass.
|
|
668
|
+
- **Every DMO reported "not ready" despite existing on the org** — the template's readiness EL expressions reference a variable (typically `isTest`) that isn't bound in your `templateValues`. Wire rule E — sentinel-inject `isTest: false` and retry. Step 2's builder does this automatically when the variable is declared on the template.
|
|
669
|
+
|
|
670
|
+
### 4. Create the app
|
|
671
|
+
|
|
672
|
+
Confirm with the user before this step — it's the first write. Show:
|
|
673
|
+
- Template apiName + label
|
|
674
|
+
- The label + apiName the new installed app will have
|
|
675
|
+
- The variables map you're about to send (values only, no secrets)
|
|
676
|
+
|
|
677
|
+
**Body construction — wire rules B + C apply:**
|
|
678
|
+
|
|
679
|
+
```bash
|
|
680
|
+
APP_NAME="${APP_NAME:-MyApp_$(date +%Y_%m_%d_%H%M)}" # user's choice — see naming rules
|
|
681
|
+
APP_LABEL="${APP_LABEL:-My App}" # user's choice
|
|
682
|
+
[ -z "$TEMPLATE_VALUES" ] && TEMPLATE_VALUES='{}' # from step 2
|
|
683
|
+
|
|
684
|
+
# ---- Pre-POST safety: enforce maxAppCount (fixes S2-I) --------------------
|
|
685
|
+
# Some templates enforce a per-org install cap (e.g. ServiceInsights=5,
|
|
686
|
+
# SalesInsightsTN=1, Employee/Service_Agent_Analytics=1, C360UnifiedSemanticModel=1).
|
|
687
|
+
# Hitting the cap surfaces reactively as
|
|
688
|
+
# `MAXAPPSEXCEEDED` from `POST /apps`. Check FIRST so the user gets a clean
|
|
689
|
+
# "you're at the cap, delete an existing app or pick a new template" message
|
|
690
|
+
# instead of a half-written client-timeout state.
|
|
691
|
+
TEMPLATE_META=$(sf api request rest "$API_BASE/templates/$TEMPLATE_NAME_OR_ID" \
|
|
692
|
+
--target-org "$ORG_ALIAS" --method GET)
|
|
693
|
+
MAX_APPS=$(echo "$TEMPLATE_META" | jq -r '.maxAppCount // .maxAppsCount // empty')
|
|
694
|
+
|
|
695
|
+
if [ -n "$MAX_APPS" ] && [ "$MAX_APPS" != "null" ]; then
|
|
696
|
+
# Count installed apps for THIS template by both the raw input AND the resolved id.
|
|
697
|
+
RESOLVED_TEMPLATE_ID=$(echo "$TEMPLATE_META" | jq -r '.id // empty')
|
|
698
|
+
CURRENT_COUNT=$(sf api request rest "$API_BASE/apps?type=App" \
|
|
699
|
+
--target-org "$ORG_ALIAS" --method GET \
|
|
700
|
+
| jq -r --arg t "$TEMPLATE_NAME_OR_ID" --arg tid "$RESOLVED_TEMPLATE_ID" '
|
|
701
|
+
(.apps // .items // .records // [])
|
|
702
|
+
| map(select(
|
|
703
|
+
(.templateSourceId == $t) or
|
|
704
|
+
(.templateSourceId == $tid) or
|
|
705
|
+
(.templateApiName == $t)
|
|
706
|
+
))
|
|
707
|
+
| length
|
|
708
|
+
')
|
|
709
|
+
if [ "$CURRENT_COUNT" -ge "$MAX_APPS" ] 2>/dev/null; then
|
|
710
|
+
echo "Refusing to create: $TEMPLATE_NAME_OR_ID enforces maxAppCount=$MAX_APPS and $CURRENT_COUNT already installed." >&2
|
|
711
|
+
echo "Delete an existing installation (step 6) or pick a different allowlisted template, then retry." >&2
|
|
712
|
+
return 1 2>/dev/null || exit 1
|
|
713
|
+
fi
|
|
714
|
+
fi
|
|
715
|
+
|
|
716
|
+
# ---- Pre-POST baseline snapshot (fixes S2-G) -------------------------------
|
|
717
|
+
# Capture the current /apps ids BEFORE the POST so timeout-recovery can identify
|
|
718
|
+
# the NEW record unambiguously — matching on name alone is fragile if a stale
|
|
719
|
+
# record with the same name exists (partial delete, propagation lag) or if a
|
|
720
|
+
# concurrent operator kicks off a same-named install in the same window.
|
|
721
|
+
BASELINE_APP_IDS=$(sf api request rest "$API_BASE/apps?type=App" \
|
|
722
|
+
--target-org "$ORG_ALIAS" --method GET \
|
|
723
|
+
| jq -c '[ (.apps // .items // .records // [])[] | .id ]')
|
|
724
|
+
BASELINE_TS=$(date +%s)
|
|
725
|
+
|
|
726
|
+
CREATE_APP_BODY_TMP=$(mktemp /tmp/create_app_body.XXXXXX)
|
|
727
|
+
CREATE_APP_BODY_FILE="${CREATE_APP_BODY_TMP}.json"
|
|
728
|
+
mv "$CREATE_APP_BODY_TMP" "$CREATE_APP_BODY_FILE"
|
|
729
|
+
|
|
730
|
+
# Compose via jq so shell interpolation never produces malformed JSON. If TEMPLATE_VALUES
|
|
731
|
+
# is {} we still include the key — the server accepts empty templateValues, and doing it
|
|
732
|
+
# this way keeps one code path for both single-click and variable-driven installs.
|
|
733
|
+
jq -n \
|
|
734
|
+
--arg tid "$TEMPLATE_NAME_OR_ID" \
|
|
735
|
+
--arg label "$APP_LABEL" \
|
|
736
|
+
--arg name "$APP_NAME" \
|
|
737
|
+
--argjson tv "$TEMPLATE_VALUES" \
|
|
738
|
+
'{templateSourceId: $tid, label: $label, name: $name, templateValues: $tv}' \
|
|
739
|
+
> "$CREATE_APP_BODY_FILE"
|
|
740
|
+
|
|
741
|
+
# If you prefer to omit templateValues entirely for a single-click install, use:
|
|
742
|
+
# jq 'del(.templateValues)' "$CREATE_APP_BODY_FILE" > "${CREATE_APP_BODY_FILE}.tmp" && mv "${CREATE_APP_BODY_FILE}.tmp" "$CREATE_APP_BODY_FILE"
|
|
743
|
+
# (Or use `sponge` from moreutils if available.) Both shapes are accepted by the v67 endpoint; keeping the key is simpler.
|
|
744
|
+
|
|
745
|
+
# ---- Step-4 target-org guard (enforces Install-target thumb rule) ---------
|
|
746
|
+
# The caller-template install ALWAYS targets $ORG_ALIAS — never HOME, never
|
|
747
|
+
# $RESOLVED_ORG_FOR_C360. Refuse if the effective target has drifted OR if the
|
|
748
|
+
# template we're about to POST is C360 itself (C360 belongs to the 2b.3 prereq
|
|
749
|
+
# path, not step 4). Rule of thumb: if $ORG_ALIAS is a COMPANION and
|
|
750
|
+
# $TEMPLATE_NAME_OR_ID is a consumer template, we MUST be targeting the
|
|
751
|
+
# companion here — a HOME target means somebody merged the C360-prereq install
|
|
752
|
+
# with the caller-template install. That's the mistake this guard catches.
|
|
753
|
+
INSTALL_TARGET_ORG="$ORG_ALIAS"
|
|
754
|
+
if [ "$TEMPLATE_NAME_OR_ID" = "sfdc_internal__C360UnifiedSemanticModel" ] \
|
|
755
|
+
&& [ "${ORG_ROLE:-UNKNOWN}" = "COMPANION" ]; then
|
|
756
|
+
cat >&2 <<MSG
|
|
757
|
+
|
|
758
|
+
REFUSING: Step 4 is about to POST a C360 install directly against companion
|
|
759
|
+
$ORG_ALIAS. C360 belongs on the paired HOME ($RESOLVED_ORG_FOR_C360), not on
|
|
760
|
+
a companion — companions consume C360 via multi-org publish, they do not host
|
|
761
|
+
the C360 app record. The C360 prereq install is driven by step 2b.3, targeting
|
|
762
|
+
\$RESOLVED_ORG_FOR_C360 automatically. If you reached step 4 with a C360
|
|
763
|
+
template on the companion, one of the following is wrong:
|
|
764
|
+
|
|
765
|
+
1. \$TEMPLATE_NAME_OR_ID was set to sfdc_internal__C360UnifiedSemanticModel
|
|
766
|
+
when the user actually asked for a different allowlisted template.
|
|
767
|
+
2. Step 2b was bypassed / short-circuited.
|
|
768
|
+
|
|
769
|
+
Re-invoke this skill with the caller template the user asked for and let
|
|
770
|
+
2b.3 handle the C360 prereq automatically.
|
|
771
|
+
MSG
|
|
772
|
+
return 1 2>/dev/null || exit 1
|
|
773
|
+
fi
|
|
774
|
+
if [ "${ORG_ROLE:-UNKNOWN}" = "COMPANION" ] \
|
|
775
|
+
&& [ -n "${HOME_ORG_ALIAS:-}" ] \
|
|
776
|
+
&& [ "$INSTALL_TARGET_ORG" = "$HOME_ORG_ALIAS" ]; then
|
|
777
|
+
cat >&2 <<MSG
|
|
778
|
+
|
|
779
|
+
REFUSING: Step 4 target-org drift detected — INSTALL_TARGET_ORG resolved to
|
|
780
|
+
\$HOME_ORG_ALIAS ($HOME_ORG_ALIAS) but \$ORG_ALIAS is $ORG_ALIAS (a COMPANION).
|
|
781
|
+
Consumer templates (SalesInsightsTN, ServiceInsights, Employee_Agent_Analytics,
|
|
782
|
+
Service_Agent_Analytics) install on the COMPANION, not on the HOME. The HOME
|
|
783
|
+
is the C360 publisher only. See "Install-target thumb rule" above.
|
|
784
|
+
|
|
785
|
+
If you truly meant to install this template on the HOME, re-invoke the skill
|
|
786
|
+
with \$ORG_ALIAS = $HOME_ORG_ALIAS (i.e. run it against the HOME alias
|
|
787
|
+
directly). Do NOT try to talk step 4 into targeting HOME from a COMPANION run.
|
|
788
|
+
MSG
|
|
789
|
+
return 1 2>/dev/null || exit 1
|
|
790
|
+
fi
|
|
791
|
+
|
|
792
|
+
RESPONSE=$(sf api request rest "$API_BASE/apps" \
|
|
793
|
+
--target-org "$INSTALL_TARGET_ORG" --method POST \
|
|
794
|
+
--body "@$CREATE_APP_BODY_FILE" \
|
|
795
|
+
--header "Content-Type: application/json")
|
|
796
|
+
|
|
797
|
+
APP_ID=$(echo "$RESPONSE" | jq -r '.id // .app.id // empty')
|
|
798
|
+
echo "New app id: $APP_ID"
|
|
799
|
+
|
|
800
|
+
rm -f "$CREATE_APP_BODY_FILE"
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
**Client-timeout fallback (sf-CLI default is 2 min).** `sf api request rest` blocks on the HTTP response even though the install itself is async. Long-running templates (ITSM, ServiceInsights, Billing Central) can exceed the client timeout even though the server has already accepted the install and started work. If `$APP_ID` comes back empty OR the CLI dies with "Command timed out after 2m 0s", recover the app id by diffing `/apps` against the baseline captured before the POST — this is unambiguous even when a stale same-named record exists:
|
|
804
|
+
|
|
805
|
+
```bash
|
|
806
|
+
if [ -z "$APP_ID" ]; then
|
|
807
|
+
echo "POST timed out client-side — attempting to recover the app id from /apps."
|
|
808
|
+
# Small backoff so the record has settled through the write path.
|
|
809
|
+
sleep 5
|
|
810
|
+
# Use `?type=App` (fixes S2-D) so we get installed apps, not templates. The
|
|
811
|
+
# unfiltered /apps endpoint on some releases returns BOTH apps and templates
|
|
812
|
+
# under the same key and matching on .name races the wrong record.
|
|
813
|
+
APPS_JSON=$(sf api request rest "$API_BASE/apps?type=App" \
|
|
814
|
+
--target-org "$ORG_ALIAS" --method GET)
|
|
815
|
+
|
|
816
|
+
# ---- Primary recovery path: baseline diff (fixes S2-G) ------------------
|
|
817
|
+
# Any id present now but NOT in $BASELINE_APP_IDS is the record this POST
|
|
818
|
+
# created. This is robust to (a) name collisions with stale records that
|
|
819
|
+
# existed before we started, and (b) concurrent operators — as long as the
|
|
820
|
+
# NEW id's .name equals $APP_NAME it's ours.
|
|
821
|
+
if [ -n "$BASELINE_APP_IDS" ]; then
|
|
822
|
+
APP_ID=$(echo "$APPS_JSON" | jq -r \
|
|
823
|
+
--argjson baseline "$BASELINE_APP_IDS" \
|
|
824
|
+
--arg name "$APP_NAME" '
|
|
825
|
+
(.apps // .items // .records // [])
|
|
826
|
+
| map(select( (.id as $i | $baseline | index($i)) | not ))
|
|
827
|
+
| map(select(.name == $name))
|
|
828
|
+
| .[0].id // empty
|
|
829
|
+
')
|
|
830
|
+
[ -n "$APP_ID" ] && echo "Recovered app id via baseline diff: $APP_ID"
|
|
831
|
+
fi
|
|
832
|
+
|
|
833
|
+
# ---- Secondary recovery path: name match (existing behavior) ------------
|
|
834
|
+
# If baseline was empty (first run) or the diff produced nothing, fall back
|
|
835
|
+
# to matching on .name alone. `name` is unique per org, so this still lands
|
|
836
|
+
# on the right record in the common case.
|
|
837
|
+
if [ -z "$APP_ID" ]; then
|
|
838
|
+
APP_ID=$(echo "$APPS_JSON" | jq -r \
|
|
839
|
+
--arg name "$APP_NAME" \
|
|
840
|
+
'(.apps // .items // .records // []) | .[]? | select(.name == $name) | .id' \
|
|
841
|
+
| head -n 1)
|
|
842
|
+
[ -n "$APP_ID" ] && echo "Recovered app id via name match: $APP_ID"
|
|
843
|
+
fi
|
|
844
|
+
|
|
845
|
+
[ -z "$APP_ID" ] && echo "Could not recover app id — check the org UI or GET $API_BASE/apps?type=App directly, and follow any .nextPageUrl if the list is paginated."
|
|
846
|
+
fi
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
Match strategy: baseline-diff FIRST (unambiguous even with name collisions or concurrent operators), name-match as fallback. `templateSourceId` on the record may be the resolved template id (`0Nk…`) even when you POSTed the apiName, so equality on the raw input breaks — that's why we don't filter on it. Do NOT retry the POST — the original request is very likely still in flight server-side, and a retry that succeeds returns `DUPLICATE_VALUE`.
|
|
850
|
+
|
|
851
|
+
The response returns the new app record and a job/status pointer. **The install is async.** `create_app` completing successfully means the job was accepted, not that the app is fully installed. Capture the returned `id` verbatim — that's what step 5's poll and step 6's rollback need.
|
|
852
|
+
|
|
853
|
+
**Naming rules the platform enforces:**
|
|
854
|
+
- `name` must be a valid Salesforce identifier: `[A-Za-z][A-Za-z0-9_]*`, ≤ 40 chars, no double underscores, no reserved suffixes (`__c`, `__mdt`, etc.).
|
|
855
|
+
- `name` must be unique on the org — reusing an existing installed-app apiName returns `DUPLICATE_VALUE`.
|
|
856
|
+
- `label` is free-text but shows up in the app launcher; keep it short and human-readable.
|
|
857
|
+
|
|
858
|
+
**Common failures:**
|
|
859
|
+
- `JSON_PARSER_ERROR: Found unexpected object property: + userinputsforsingleclickdeploy` — you're using an old MCP-tool schema that leaks the wrong param name to the wire. Rename to `templateValues` at wire level. Wire rule B.
|
|
860
|
+
- `DUPLICATE_VALUE` — apiName is taken. Pick a new apiName or `delete_app` the existing one if user confirms.
|
|
861
|
+
- `BAD_REQUEST: variable name [dataSpaceApiName] ... not of expected type [String]` — wire rule C again.
|
|
862
|
+
|
|
863
|
+
### 5. Poll for install completion
|
|
864
|
+
|
|
865
|
+
Installs are async. Wire rule D applies — read `applicationStatus`, not `status`.
|
|
866
|
+
|
|
867
|
+
Wire-observed status vocabulary (as of 2026-08-24): `SuccessStatus`, `SuccessWithWarningsStatus`, `FailedStatus`, `InProgressStatus`, plus several in-flight variants that the platform emits at different phases of the chain — `NewStatus`, `QueuedStatus`, `PendingStatus`, `AwaitingActivationStatus`. Anything ending in `InProgressStatus`, `PendingStatus`, `QueuedStatus`, `NewStatus`, or `AwaitingActivationStatus` is in-flight and MUST keep the poll running; only `Success*` / `Failed*` are terminal. Treating an unknown suffix as terminal would break the poll on future in-flight variants — instead, default to KEEP-POLLING and require an explicit terminal-suffix match to break.
|
|
868
|
+
|
|
869
|
+
Failure detail fields also vary — the platform has shipped `failureReasons` (older), `failureReason` (singular), `installErrorMessage`, `errorMessage`, and `errors[].message` at different points. The dump below tries all of them and falls back to the raw payload so a `null` field never eats the actual error text.
|
|
870
|
+
|
|
871
|
+
```bash
|
|
872
|
+
POLL_INTERVAL=15
|
|
873
|
+
MAX_MINUTES=30
|
|
874
|
+
DEADLINE=$(( $(date +%s) + MAX_MINUTES * 60 ))
|
|
875
|
+
|
|
876
|
+
# Treat any status ending in one of these tokens as in-flight — future variants land here safely.
|
|
877
|
+
is_inflight() {
|
|
878
|
+
case "$1" in
|
|
879
|
+
*InProgressStatus|*PendingStatus|*QueuedStatus|*NewStatus|*AwaitingActivationStatus) return 0 ;;
|
|
880
|
+
*) return 1 ;;
|
|
881
|
+
esac
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
while true; do
|
|
885
|
+
APP=$(sf api request rest "$API_BASE/apps/$APP_ID" \
|
|
886
|
+
--target-org "$ORG_ALIAS" --method GET)
|
|
887
|
+
APP_STATUS=$(echo "$APP" | jq -r '.applicationStatus // "Unknown"')
|
|
888
|
+
echo "$(date '+%H:%M:%S') applicationStatus=$APP_STATUS"
|
|
889
|
+
|
|
890
|
+
case "$APP_STATUS" in
|
|
891
|
+
SuccessStatus)
|
|
892
|
+
echo "Installed. Done."; break ;;
|
|
893
|
+
SuccessWithWarningsStatus)
|
|
894
|
+
echo "Installed with warnings:"
|
|
895
|
+
# Dump the FULL app record — the platform has shipped .warnings, .installWarnings,
|
|
896
|
+
# and .validationWarnings at different times; the fallback to `.` prints the raw
|
|
897
|
+
# payload if none of the named fields carry the warning text.
|
|
898
|
+
echo "$APP" | jq '{applicationStatus, warnings: (.warnings // .installWarnings // .validationWarnings // null), raw: .}'
|
|
899
|
+
break ;;
|
|
900
|
+
FailedStatus)
|
|
901
|
+
# The failure-detail field has drifted across releases — try every known field
|
|
902
|
+
# and print all non-null ones, then include the raw record as a belt-and-braces
|
|
903
|
+
# so an unknown field name never surfaces as a bare `null`.
|
|
904
|
+
echo "Install FAILED. Detail:"
|
|
905
|
+
echo "$APP" | jq '{
|
|
906
|
+
applicationStatus,
|
|
907
|
+
failureReasons: (.failureReasons // null),
|
|
908
|
+
failureReason: (.failureReason // null),
|
|
909
|
+
installErrorMessage: (.installErrorMessage // null),
|
|
910
|
+
errorMessage: (.errorMessage // null),
|
|
911
|
+
errors: (.errors // null),
|
|
912
|
+
raw: .
|
|
913
|
+
}'
|
|
914
|
+
break ;;
|
|
915
|
+
Unknown|"")
|
|
916
|
+
echo "Unexpected shape (no applicationStatus field):"
|
|
917
|
+
echo "$APP" | jq '.'
|
|
918
|
+
break ;;
|
|
919
|
+
*)
|
|
920
|
+
if is_inflight "$APP_STATUS"; then
|
|
921
|
+
# In-flight variant (NewStatus, QueuedStatus, PendingStatus,
|
|
922
|
+
# AwaitingActivationStatus, *InProgressStatus). Keep polling — the platform
|
|
923
|
+
# can emit any of these before landing on a terminal Success*/Failed* state.
|
|
924
|
+
:
|
|
925
|
+
else
|
|
926
|
+
# Truly unrecognized — dump and stop rather than looping forever.
|
|
927
|
+
echo "Unrecognized status '$APP_STATUS' (neither terminal nor a known in-flight suffix). Dumping and stopping."
|
|
928
|
+
echo "$APP" | jq '.'
|
|
929
|
+
break
|
|
930
|
+
fi
|
|
931
|
+
;;
|
|
932
|
+
esac
|
|
933
|
+
|
|
934
|
+
if [ "$(date +%s)" -ge "$DEADLINE" ]; then
|
|
935
|
+
echo "Timed out after $MAX_MINUTES min at status=$APP_STATUS"
|
|
936
|
+
break
|
|
937
|
+
fi
|
|
938
|
+
sleep "$POLL_INTERVAL"
|
|
939
|
+
done
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
Typical install duration: 3–7 min for single-DMO templates, 8–30 min for full-chain templates. Service Insights and Sales Insights TN are typically 8–15 min; Employee / Service Agent Analytics are chain-heavier and can run 15–25 min; C360 Semantic Model is typically 5–10 min. Set `MAX_MINUTES=30` (as above) for the allowlisted five.
|
|
943
|
+
|
|
944
|
+
### 6. Rollback on failure (optional but recommended)
|
|
945
|
+
|
|
946
|
+
If step 5 landed on `FailedStatus`, or the user changes their mind, delete the app:
|
|
947
|
+
|
|
948
|
+
```bash
|
|
949
|
+
# sf-CLI quirk: DELETE requires `--body '{}'` even though HTTP DELETE has no body.
|
|
950
|
+
# Omitting --body raises `SfError: No 'mode' found in 'body' entry` before the
|
|
951
|
+
# request is ever made. `{}` is the sentinel the CLI's HTTP wrapper expects.
|
|
952
|
+
sf api request rest "$API_BASE/apps/$APP_ID" \
|
|
953
|
+
--target-org "$ORG_ALIAS" --method DELETE \
|
|
954
|
+
--body '{}' \
|
|
955
|
+
| jq '.'
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
`delete_app` is idempotent — deleting the same app twice is a no-op after the first success. Safe to retry on transient network faults. **Confirm with the user** before deleting an app that already reached `SuccessStatus` (they may be actively using it).
|
|
959
|
+
|
|
960
|
+
**Server-side propagation is async.** The DELETE returns 202/success quickly, but the app can take several minutes to fully unwind. Subsequent `GET /apps/{id}` returns `APPNOTFOUND` once it settles. If you're deleting to free a slot before a fresh install (e.g. `MAXAPPSEXCEEDED` on ITSM), poll `GET /apps/{id}` until `APPNOTFOUND` before retrying the create.
|
|
961
|
+
|
|
962
|
+
**DELETE returns HTTP 204 but the record refuses to unwind (active-flow blocker).** Wire-verified 2026-08-25 on a ServiceInsights install: the App Framework delete chain fans out to a `BulkDeleteAssets` job that walks every asset the failed install created — flow versions included. If ANY asset in that chain is a **flow version stuck in Active state**, the platform throws `CannotDeleteException: You cannot delete this flow version because it is active. Deactivate it, and try again.` inside the async job. The public DELETE call has ALREADY returned 204 because the request was accepted; the failure is server-side, in the background chain. `GET /apps/{id}` keeps returning the record indefinitely because no delete job ever completes.
|
|
963
|
+
|
|
964
|
+
**Diagnostic path (this is the ONLY reliable signal from the wire):**
|
|
965
|
+
1. Open the org UI: **Setup → App Hub** (search `app hub` in Setup). The stuck app appears with a `Failed` job named `BulkDeleteAssets` or `delete_tua`.
|
|
966
|
+
2. Expand the failed job — the right-hand pane shows `Failed to delete asset [301S…] reason [You cannot delete this flow version because it is active. Deactivate it, and try again.]`. The `301S…` prefix identifies a `Flow` sobject.
|
|
967
|
+
3. **Fix path:** In the org UI, navigate to **Setup → Flows**, find every flow whose API name matches an asset the template installed (Service Insights ships flows like `Contact_Reason_Data_Population`, `Case_Contact_Reason_Setup`, etc.), and click **Deactivate** on the active version. Confirm zero remaining `Active` versions.
|
|
968
|
+
4. Re-issue the DELETE (`sf api request rest "$API_BASE/apps/$STALE_ID" --method DELETE --body '{}' -i` — expect `HTTP/1.1 204` again). Within 30–60 s, `GET /apps?type=App` no longer returns the record.
|
|
969
|
+
|
|
970
|
+
**Wire-only detection is not possible in the current OAS.** Neither `GET /apps/{id}` nor `GET /apps/{id}/activities` surfaces the underlying `CannotDeleteException` in a machine-readable field — the only way to see it is the App Hub UI. When you observe DELETE-succeeded-but-record-persists for **more than ~5 minutes** after two attempts, assume active-flow (or another `CannotDelete*` asset) and route the user to App Hub with the exact steps above. Do NOT keep polling silently; the record will never unwind on its own.
|
|
971
|
+
|
|
972
|
+
**Why the fresh in-session FailedStatus deleted cleanly and the stale one didn't.** A fresh failure typically didn't get far enough down the create chain to touch flows — the asset set is thin, and DELETE unwinds seconds later. Stale FailedStatus records from a prior session often reached the "activate flows" step before failing, leaving active flows behind that block the delete-chain. Age of the record is a strong signal (5+ days old + won't unwind = suspect active flows first).
|
|
973
|
+
|
|
974
|
+
**Delete-by-name (fixes S2-H).** If you lost the app id — the create-app POST timed out client-side AND both baseline-diff and name-match recovery came back empty — the ONLY handle you have is the `name` the user supplied. Resolve the id from `/apps?type=App` and then delete:
|
|
975
|
+
|
|
976
|
+
```bash
|
|
977
|
+
# Usage: delete_app_by_name "$APP_NAME"
|
|
978
|
+
# Returns 0 on delete accepted (or when the app already didn't exist);
|
|
979
|
+
# returns 1 on ambiguous match (>1 app with the same .name) so the caller
|
|
980
|
+
# can escalate rather than delete the wrong record.
|
|
981
|
+
delete_app_by_name() {
|
|
982
|
+
local target_name="$1"
|
|
983
|
+
[ -z "$target_name" ] && { echo "delete_app_by_name: name required" >&2; return 2; }
|
|
984
|
+
|
|
985
|
+
local apps_json id_count target_ids target_id
|
|
986
|
+
apps_json=$(sf api request rest "$API_BASE/apps?type=App" \
|
|
987
|
+
--target-org "$ORG_ALIAS" --method GET)
|
|
988
|
+
|
|
989
|
+
target_ids=$(echo "$apps_json" | jq -r \
|
|
990
|
+
--arg name "$target_name" \
|
|
991
|
+
'(.apps // .items // .records // []) | .[]? | select(.name == $name) | .id')
|
|
992
|
+
id_count=$(printf '%s\n' "$target_ids" | grep -c . || true)
|
|
993
|
+
|
|
994
|
+
if [ "$id_count" = "0" ]; then
|
|
995
|
+
echo "No app named '$target_name' found — nothing to delete." >&2
|
|
996
|
+
return 0
|
|
997
|
+
fi
|
|
998
|
+
if [ "$id_count" != "1" ]; then
|
|
999
|
+
echo "Ambiguous: $id_count apps named '$target_name' exist. Refusing to delete." >&2
|
|
1000
|
+
echo "Resolve manually via GET $API_BASE/apps?type=App and pick the correct id." >&2
|
|
1001
|
+
return 1
|
|
1002
|
+
fi
|
|
1003
|
+
|
|
1004
|
+
target_id=$(printf '%s\n' "$target_ids" | head -n 1)
|
|
1005
|
+
echo "Deleting app '$target_name' (id=$target_id)..."
|
|
1006
|
+
sf api request rest "$API_BASE/apps/$target_id" \
|
|
1007
|
+
--target-org "$ORG_ALIAS" --method DELETE \
|
|
1008
|
+
--body '{}' \
|
|
1009
|
+
| jq '.'
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
Only use `delete_app_by_name` when the id is genuinely lost — id-based DELETE is always preferable. The name-based path is deliberately strict about ambiguity: if the same name resolves to more than one record (partial delete state, race with a concurrent operator), it refuses rather than picking one at random.
|
|
1014
|
+
|
|
1015
|
+
### 6b. Update an existing app (API-driven Reconfigure)
|
|
1016
|
+
|
|
1017
|
+
`PUT /app-framework/apps/{id}` is the API-driven equivalent of App Hub → tile → **Update** / **Reconfigure**. It re-runs the template's install chain against the existing app record, upgrading it to the template's *current* `templateVersion` and re-applying `templateValues`.
|
|
1018
|
+
|
|
1019
|
+
**Primary use-case for this skill: auto-remediating a 2b.5 C360 template-version mismatch on the HOME org** — instead of stopping the caller's install and printing the App Hub Reconfigure walkthrough, the skill can (when the caller has confirmed) drive the update itself against `$RESOLVED_ORG_FOR_C360`, poll to `SuccessStatus`, and then resume the caller's install without a UI hop. Wire rule I above spells out the body shape; the helper below packages it.
|
|
1020
|
+
|
|
1021
|
+
**Guardrails (do NOT skip these):**
|
|
1022
|
+
- **HOME-only.** The PUT MUST target the org that owns the app record. On the C360-mismatch remediation path that is `$RESOLVED_ORG_FOR_C360` (== `$ORG_ALIAS` when `ORG_ROLE=HOME`; == `$HOME_ORG_ALIAS` when `ORG_ROLE=COMPANION`). A companion session cannot mutate a HOME's app — cross-org sessions do not exist.
|
|
1023
|
+
- **Companion blast radius.** The HOME's C360 app is shared by every paired companion. When called from a companion, this helper MUST confirm with the caller before running — auto-remediating on their behalf mutates a shared resource for every other companion pointing at the same HOME. The default is REFUSE + surface App Hub instructions; only PUT if the caller has explicitly opted in for this invocation.
|
|
1024
|
+
- **Reuse the step-2 builder for `templateValues`.** Do not hand-craft a different body — the same required-var filter + sentinel injection + `isTest` inject + `{{Variables.X.Y}}` resolver + data-space resolution + NumberType decimal coercion must apply, or the update chain will 400 with the same errors create would.
|
|
1025
|
+
- **`templateSourceId` from the existing app record, NOT from an assumed constant.** Read it from `GET /apps/{id}` and pass it through verbatim. The C360 template apiName is stable across releases today, but hardcoding it in the helper is a footgun for the day another allowlisted template needs update-in-place too.
|
|
1026
|
+
- **Do NOT include `label` or `name` in the body.** Those are create-time fields and are read-only on update.
|
|
1027
|
+
- **Auth.** Same as create: caller needs `AppFrameworkManageTemplate` on the target org. If PUT returns 403, the caller (or an admin on their behalf) is missing the permset that carries it — same remediation as precondition 2.
|
|
1028
|
+
|
|
1029
|
+
```bash
|
|
1030
|
+
# ---- 6b. Update an existing app -------------------------------------------
|
|
1031
|
+
# Usage:
|
|
1032
|
+
# update_app "$APP_ID" "$TARGET_ORG_ALIAS" "$TEMPLATE_VALUES_JSON"
|
|
1033
|
+
#
|
|
1034
|
+
# $TEMPLATE_VALUES_JSON is the fully-built templateValues map from step 2
|
|
1035
|
+
# (post required-filter, post isTest inject, post placeholder resolution, post
|
|
1036
|
+
# NumberType decimal coercion). It's the SAME value that would go into the
|
|
1037
|
+
# CREATE body's `templateValues` field — the update endpoint accepts it byte
|
|
1038
|
+
# for byte.
|
|
1039
|
+
#
|
|
1040
|
+
# On success the function prints the app id, sets $UPDATED_APP_ID as a
|
|
1041
|
+
# side-effect, and returns 0. The caller then polls $API_BASE/apps/$APP_ID
|
|
1042
|
+
# with the same loop as step 5 until `applicationStatus` is terminal
|
|
1043
|
+
# (SuccessStatus / SuccessWithWarningsStatus / FailedStatus).
|
|
1044
|
+
|
|
1045
|
+
update_app() {
|
|
1046
|
+
local app_id="$1"
|
|
1047
|
+
local target_org="$2"
|
|
1048
|
+
local template_values_json="$3"
|
|
1049
|
+
|
|
1050
|
+
if [ -z "$app_id" ] || [ -z "$target_org" ]; then
|
|
1051
|
+
echo "update_app: app_id and target_org are required." >&2
|
|
1052
|
+
return 2
|
|
1053
|
+
fi
|
|
1054
|
+
# Default to `{}` if the caller passes an empty/blank templateValues — the
|
|
1055
|
+
# server accepts empty templateValues on update (nothing to change), same as
|
|
1056
|
+
# create. Keeps one code path.
|
|
1057
|
+
if [ -z "$template_values_json" ]; then
|
|
1058
|
+
template_values_json='{}'
|
|
1059
|
+
fi
|
|
1060
|
+
|
|
1061
|
+
# Look up the current app record to (a) confirm it exists on this org and
|
|
1062
|
+
# (b) source the templateSourceId. Do NOT hardcode the apiName here — some
|
|
1063
|
+
# allowlisted templates live under different apiNames across releases.
|
|
1064
|
+
local current_app_json template_source_id current_status
|
|
1065
|
+
current_app_json=$(sf api request rest "$API_BASE/apps/$app_id" \
|
|
1066
|
+
--target-org "$target_org" --method GET 2>/dev/null || true)
|
|
1067
|
+
|
|
1068
|
+
# `.templateSourceId` is the canonical field; some releases expose the same
|
|
1069
|
+
# value at `.templateApiName`. Prefer templateSourceId when both are present.
|
|
1070
|
+
template_source_id=$(echo "$current_app_json" | jq -r '
|
|
1071
|
+
.templateSourceId // .templateApiName // empty
|
|
1072
|
+
')
|
|
1073
|
+
current_status=$(echo "$current_app_json" | jq -r '
|
|
1074
|
+
.applicationStatus // .status // empty
|
|
1075
|
+
')
|
|
1076
|
+
|
|
1077
|
+
if [ -z "$template_source_id" ]; then
|
|
1078
|
+
echo "update_app: could not resolve templateSourceId from GET /apps/$app_id on $target_org." >&2
|
|
1079
|
+
echo "Raw response follows:" >&2
|
|
1080
|
+
echo "$current_app_json" | jq '.' >&2 2>/dev/null || echo "$current_app_json" >&2
|
|
1081
|
+
return 1
|
|
1082
|
+
fi
|
|
1083
|
+
|
|
1084
|
+
# Refuse the PUT if the app is currently mid-install. The server would 409
|
|
1085
|
+
# ("app is being modified"), but a clean up-front refusal keeps the message
|
|
1086
|
+
# actionable — poll to terminal first, THEN update.
|
|
1087
|
+
case "$current_status" in
|
|
1088
|
+
InProgressStatus|PendingStatus|NewStatus)
|
|
1089
|
+
echo "update_app: app $app_id on $target_org is in $current_status — refusing PUT." >&2
|
|
1090
|
+
echo "Wait for the app to reach a terminal status (Success*/Failed*) before updating." >&2
|
|
1091
|
+
return 1
|
|
1092
|
+
;;
|
|
1093
|
+
esac
|
|
1094
|
+
|
|
1095
|
+
# Compose the PUT body via jq so `--body @file` gets valid JSON regardless
|
|
1096
|
+
# of what shell interpolation might do to embedded quotes/backslashes.
|
|
1097
|
+
local update_body_file="${TMPDIR:-/tmp}/update_app_${app_id}.$$.json"
|
|
1098
|
+
jq -n \
|
|
1099
|
+
--arg tsid "$template_source_id" \
|
|
1100
|
+
--argjson tv "$template_values_json" \
|
|
1101
|
+
'{templateSourceId: $tsid, templateValues: $tv}' \
|
|
1102
|
+
> "$update_body_file"
|
|
1103
|
+
|
|
1104
|
+
echo "PUT $API_BASE/apps/$app_id (target-org=$target_org, templateSourceId=$template_source_id) ..." >&2
|
|
1105
|
+
|
|
1106
|
+
local put_response
|
|
1107
|
+
put_response=$(sf api request rest "$API_BASE/apps/$app_id" \
|
|
1108
|
+
--target-org "$target_org" --method PUT \
|
|
1109
|
+
--body "@$update_body_file")
|
|
1110
|
+
local rc=$?
|
|
1111
|
+
rm -f "$update_body_file"
|
|
1112
|
+
|
|
1113
|
+
if [ "$rc" != "0" ]; then
|
|
1114
|
+
echo "update_app: PUT failed (rc=$rc). Raw response:" >&2
|
|
1115
|
+
echo "$put_response" >&2
|
|
1116
|
+
return 1
|
|
1117
|
+
fi
|
|
1118
|
+
|
|
1119
|
+
echo "$put_response" | jq '.' >&2 2>/dev/null || echo "$put_response" >&2
|
|
1120
|
+
|
|
1121
|
+
# Sanity: the response should carry the same app id. Surface a warning if not
|
|
1122
|
+
# — caller-side polling still works via the passed-in $app_id.
|
|
1123
|
+
local returned_id
|
|
1124
|
+
returned_id=$(echo "$put_response" | jq -r '.id // .app.id // empty')
|
|
1125
|
+
if [ -n "$returned_id" ] && [ "$returned_id" != "$app_id" ]; then
|
|
1126
|
+
echo "update_app: WARNING — response .id ($returned_id) != input app_id ($app_id)." >&2
|
|
1127
|
+
fi
|
|
1128
|
+
|
|
1129
|
+
UPDATED_APP_ID="$app_id"
|
|
1130
|
+
return 0
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
# ---- Poll the update to terminal (reuse step 5's shape) --------------------
|
|
1134
|
+
# The update runs asynchronously — after PUT returns, poll GET /apps/{id}
|
|
1135
|
+
# until applicationStatus is terminal. Same wire rule D as step 5 (check
|
|
1136
|
+
# .applicationStatus, not .status; treat *InProgress*/*Pending*/*New* as
|
|
1137
|
+
# in-flight; treat Success*/Failed*/Cancelled as terminal).
|
|
1138
|
+
poll_update_to_terminal() {
|
|
1139
|
+
local app_id="$1"
|
|
1140
|
+
local target_org="$2"
|
|
1141
|
+
local cap_secs="${3:-1800}" # default 30 min
|
|
1142
|
+
|
|
1143
|
+
local deadline status app_resp
|
|
1144
|
+
deadline=$(( $(date +%s) + cap_secs ))
|
|
1145
|
+
while [ "$(date +%s)" -lt "$deadline" ]; do
|
|
1146
|
+
app_resp=$(sf api request rest "$API_BASE/apps/$app_id" \
|
|
1147
|
+
--target-org "$target_org" --method GET 2>/dev/null || true)
|
|
1148
|
+
status=$(echo "$app_resp" | jq -r '.applicationStatus // .status // empty')
|
|
1149
|
+
case "$status" in
|
|
1150
|
+
SuccessStatus|SuccessWithWarningsStatus)
|
|
1151
|
+
echo "[6b] update SUCCEEDED: $app_id -> $status" >&2
|
|
1152
|
+
return 0
|
|
1153
|
+
;;
|
|
1154
|
+
FailedStatus|CancelledStatus)
|
|
1155
|
+
echo "[6b] update TERMINAL FAILURE: $app_id -> $status" >&2
|
|
1156
|
+
echo "$app_resp" | jq '.failureReasons // .' >&2 2>/dev/null || echo "$app_resp" >&2
|
|
1157
|
+
return 1
|
|
1158
|
+
;;
|
|
1159
|
+
InProgressStatus|PendingStatus|NewStatus|"")
|
|
1160
|
+
sleep 30
|
|
1161
|
+
;;
|
|
1162
|
+
*)
|
|
1163
|
+
# Unknown / release-drift status — sleep and retry a few times before
|
|
1164
|
+
# treating as unknown-terminal.
|
|
1165
|
+
echo "[6b] unknown applicationStatus '$status' on $app_id — waiting." >&2
|
|
1166
|
+
sleep 30
|
|
1167
|
+
;;
|
|
1168
|
+
esac
|
|
1169
|
+
done
|
|
1170
|
+
|
|
1171
|
+
echo "[6b] update did not reach terminal status within ${cap_secs}s on $target_org." >&2
|
|
1172
|
+
return 1
|
|
1173
|
+
}
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
**Auto-remediation hook in 2b.5.** When the `CMP=-1` branch fires on `ORG_ROLE=HOME` (or on `ORG_ROLE=COMPANION` with explicit opt-in — see below), 2b.5 AUTOMATICALLY calls `update_app "$INSTALLED_C360_APP_ID" "$RESOLVED_ORG_FOR_C360" '{}'` followed by `poll_update_to_terminal "$INSTALLED_C360_APP_ID" "$RESOLVED_ORG_FOR_C360" 1800` before printing any user-facing STOP message. Pass an empty `{}` for `templateValues` — `$TEMPLATE_VALUES_RAW` holds the CALLING template's values, which are the wrong body for a C360 app update (same zero-inputs shape as `install_c360_on_home_and_wait_for_sync`'s create body). On success, re-fetch `INSTALLED_TEMPLATE_VERSION`/`CURRENT_TEMPLATE_VERSION` against `$RESOLVED_ORG_FOR_C360`, re-run `compare_versions`, and — on parity — **also require that the updated C360 SDM be visible in the caller's Data Cloud before flipping `AUTO_UPDATE_PARITY_REACHED=true`**. HOME path: the SDM is local and readiness will re-read it directly, so no additional wait is needed. COMPANION path: the updated SDM must PROPAGATE from `$RESOLVED_ORG_FOR_C360` (== HOME) to `$ORG_ALIAS` (== caller) via multi-org sync — 2b.5 calls `wait_for_c360_sdm_visible "$ORG_ALIAS" 0 "COMPANION post-update propagation" "$RESOLVED_ORG_FOR_C360"` (UNBOUNDED, same discipline as the install-time wait per the user's "keep polling till the SDM is available" instruction). Only after the wait returns 0 does the parity flag flip. If `update_app` returns non-zero (auth, missing app record, mid-flight, wire error), or `poll_update_to_terminal` times out, or the post-update version re-fetch does not confirm parity, or the post-update SDM-visibility wait exits on auth error, fall through to the manual Option A/B STOP message + wait-for-parity poll (the update may still be catching up server-side; the loop will pick it up). Under bash `case` the `break` keyword only exits an enclosing loop, so the parity-reached signal MUST be a flag (`AUTO_UPDATE_PARITY_REACHED`) that guards the STOPPING + wait-for-parity `while` + refusal block via an `if` — NOT a `break` statement inside the `case`.
|
|
1177
|
+
|
|
1178
|
+
**COMPANION opt-in gate for 2b.5.** On `ORG_ROLE=COMPANION` the auto-update is refused by default because `$RESOLVED_ORG_FOR_C360` is a shared HOME. Two opt-in signals unlock the same PUT + poll code path: (1) `TABNEXT_ALLOW_SHARED_HOME_C360_UPDATE=1` in the environment — appropriate for scripted / unattended runs where the operator has pre-authorized the shared-HOME reconfigure across all paired companions; (2) an interactive TTY confirm — when both stdin and stderr are TTYs, 2b.5 prints a blast-radius warning and prompts "Proceed with shared-HOME C360 update? [y/N]" with a 60 s `read -t` timeout; anything except `y/Y/yes/YES/Yes` (including timeout, empty, or `n`) declines. Declined opt-in falls through to the standard STOP + wait-for-parity path. When the opt-in is granted, log lines carry `role=COMPANION opt-in=env|tty` so the transcript records how the shared-HOME reconfigure was authorized. Absent both signals (no env var + no TTY, e.g. CI without a pre-set env var), the default is refuse.
|
|
1179
|
+
|
|
1180
|
+
**Companion HOME-alias auto-resume in `resolve_c360_gate_orgs()`.** When 2b.0 classifies the caller as COMPANION but no local `sf` alias resolves for `HOME_ORG_ID_15`, the helper prints the full onboarding STOPPING message (my-domain URL / permissions / `sf org login web` command / verify-alias jq recipe) AND then enters a bounded auto-resume loop (30 s cadence, 15-min cap) that re-runs the `sf org list --json | jq` alias lookup. As soon as the admin completes the `sf org login web` in another terminal on the same laptop, the loop picks up the newly-registered alias and returns with `HOME_ORG_ALIAS` set — the current run continues into 2b.3/2b.4/2b.5 without a re-invoke. If the cap elapses, the helper refuses with an explicit "did not appear within 15 min" message and the caller must re-invoke (Step 2b.0 will then re-probe DataMultiOrgConnection). The 30 s cadence + 15 min cap is calibrated for the typical browser-login round-trip (30–90 s) and avoids wedging on a stuck admin.
|
|
1181
|
+
|
|
1182
|
+
The wait-for-parity poll loop remains the outer safety net — it handles the case where the caller declined the auto-update, the caller is on the companion path with no explicit opt-in, or the update was driven by another process (a separate operator clicking Update in App Hub concurrently). PUT is a *shortcut*, not a *substitute*.
|
|
1183
|
+
|
|
1184
|
+
## Fast path (optional) — use MCP tools if they're connected
|
|
1185
|
+
|
|
1186
|
+
**The allowlist gate applies to this path too.** Before invoking any MCP tool that installs, creates, or checks readiness on a template, run `resolve_allowed_template` on the user's input AND run the step 1 double-gate check (Path B id resolution + `IS_ALLOWED` re-check). MCP tools do NOT enforce the allowlist internally — they wrap the raw App-Framework endpoints, so a `templateSourceId` outside the allowlist will install just as easily via `mcp__tableau-next-apps-mcp-beta__create_app` as via a raw `sf api request rest` call. The gate lives in THIS skill, not in the tools.
|
|
1187
|
+
|
|
1188
|
+
If any of these MCP servers are connected AND healthy, the caller MAY substitute them for the `sf api request rest` calls in steps 1–6 (**after passing the allowlist gate**) to save time on argument construction. The wire-shape rules A–D above still apply verbatim — these tools are thin wrappers around the same endpoints and inherit the same server-side validation.
|
|
1189
|
+
|
|
1190
|
+
| Step | project-codey preset | tableau-next-apps-mcp / -beta preset |
|
|
1191
|
+
|---|---|---|
|
|
1192
|
+
| 1 | `mcp__project-codey__list_tabnext_app_templates` | `mcp__tableau-next-apps-mcp-beta__list_tabnext_app_templates` |
|
|
1193
|
+
| 2 | `mcp__project-codey__fetch_template_required_variables` | `mcp__tableau-next-apps-mcp-beta__fetch_template_required_variables` |
|
|
1194
|
+
| 3 | `mcp__project-codey__check_template_readiness` | `mcp__tableau-next-apps-mcp-beta__check_template_readiness` |
|
|
1195
|
+
| 4 | `mcp__project-codey__create_app` | `mcp__tableau-next-apps-mcp-beta__create_app` |
|
|
1196
|
+
| 5 | `mcp__project-codey__dispatch_readonly` GET `/apps/{id}` (MVP1 ships no `get_app_status` preset) | `mcp__tableau-next-apps-mcp-beta__get_app_status` |
|
|
1197
|
+
| 6 | `mcp__project-codey__delete_app` | `mcp__tableau-next-apps-mcp-beta__delete_app` |
|
|
1198
|
+
|
|
1199
|
+
**MCP-tool caveats:**
|
|
1200
|
+
- Both wrappers were audited and fixed on 2026-08-18 (Python `install_create_app.py` `"values"`→`"templateValues"`; Java `InstallCheckTemplateReadinessHandler` always wraps in `templateValues`; `drive_install_flow.py` / `drive_datakit_deploy.py` use bare-string `dataSpaceApiName`). If you're on those or later builds, wire rules B and C are honored transparently. If you're on an older `project-codey` MCP where `create_app` still advertises `userInputsForSingleClickDeploy`, invoke `mcp__project-codey__dispatch` directly with a POST body that uses the correct OAS key `templateValues` (wire rule B) instead of the preset.
|
|
1201
|
+
- If ANY MCP tool call sits silent for > 90s with no progress, treat it as timed out and fall back to `sf api request rest` for the remainder of the flow. The whole reason this skill is self-contained is to survive that failure mode.
|
|
1202
|
+
|
|
1203
|
+
## End-to-end example (Service Insights — allowlisted)
|
|
1204
|
+
|
|
1205
|
+
```text
|
|
1206
|
+
User: "install Service Insights on my org"
|
|
1207
|
+
|
|
1208
|
+
Skill (this skill, running self-contained):
|
|
1209
|
+
|
|
1210
|
+
# 0. Discover API version
|
|
1211
|
+
$ API_VERSION=$(sf api request rest "/services/data" --target-org my-org --method GET \
|
|
1212
|
+
| jq -r '[.[] | .version | tonumber | floor] | max | "v\(.).0"')
|
|
1213
|
+
→ v67.0 (or whatever the org actually supports)
|
|
1214
|
+
$ API_BASE="/services/data/$API_VERSION/app-framework"
|
|
1215
|
+
|
|
1216
|
+
# 0a. Allowlist gate — resolve user input to a canonical apiName
|
|
1217
|
+
$ TEMPLATE_NAME_OR_ID=$(resolve_allowed_template "service insights")
|
|
1218
|
+
→ sfdc_internal__ServiceInsights
|
|
1219
|
+
# If the user had asked for "install ITSM" or "install Billing Central", this
|
|
1220
|
+
# would have printed REFUSED and the skill would have stopped here.
|
|
1221
|
+
|
|
1222
|
+
# 1. Confirm the template exists on this org + capture id
|
|
1223
|
+
$ sf api request rest "$API_BASE/templates" --target-org my-org --method GET \
|
|
1224
|
+
| jq --arg n "$TEMPLATE_NAME_OR_ID" '.templates[] | select(.name == $n)'
|
|
1225
|
+
→ { id: "0Nk…", name: "sfdc_internal__ServiceInsights", label: "Service Insights", ... }
|
|
1226
|
+
Confirm with user: "Install Service Insights (sfdc_internal__ServiceInsights)?"
|
|
1227
|
+
|
|
1228
|
+
# 2. Variables come from GET /configuration — .variables is an object map at the top
|
|
1229
|
+
# of the response; required==true + always-inject isTest go into templateValues
|
|
1230
|
+
$ sf api request rest "$API_BASE/templates/sfdc_internal__ServiceInsights/configuration" \
|
|
1231
|
+
--target-org my-org --method GET
|
|
1232
|
+
→ { variables: { "dataSpaceApiName": { defaultValue: null, required: true,
|
|
1233
|
+
variableType: {type: "DataSpaceType"}, ... },
|
|
1234
|
+
... },
|
|
1235
|
+
...other configuration keys... }
|
|
1236
|
+
# Resolve data space name FIRST (wire rule G):
|
|
1237
|
+
$ DATA_SPACE_NAME=$(sf api request rest "/services/data/$API_VERSION/ssot/data-spaces" \
|
|
1238
|
+
--target-org my-org --method GET | jq -r '(.dataSpaces // [] | .[0].name) // "default"')
|
|
1239
|
+
→ "default" (or "Corp_Ds", whatever the org actually has)
|
|
1240
|
+
|
|
1241
|
+
# Step 2's builder (required filter + type-aware sentinels + always-inject isTest + placeholder resolution
|
|
1242
|
+
# + NumberType float coercion in Pass 1b):
|
|
1243
|
+
# ServiceInsights's dataSpaceApiName is DataSpaceType → object form (wire rule C)
|
|
1244
|
+
# isTest injected unconditionally as false (wire rule E)
|
|
1245
|
+
# TEMPLATE_VALUES = {"dataSpaceApiName":{"dataSpaceName":"default"},"isTest":false}
|
|
1246
|
+
|
|
1247
|
+
# 3. Readiness — always wrap in templateValues (wire rule A)
|
|
1248
|
+
$ echo '{"templateValues":{"dataSpaceApiName":{"dataSpaceName":"default"},"isTest":false}}' > /tmp/rb.json
|
|
1249
|
+
$ sf api request rest "$API_BASE/templates/sfdc_internal__ServiceInsights/readiness" \
|
|
1250
|
+
--target-org my-org --method POST --body '@/tmp/rb.json' --header "Content-Type: application/json"
|
|
1251
|
+
→ { Status: "Success" }
|
|
1252
|
+
|
|
1253
|
+
# 4. Create app (confirm write with user first)
|
|
1254
|
+
# - Pre-POST: check maxAppCount (ServiceInsights=5) and snapshot /apps?type=App baseline.
|
|
1255
|
+
$ cat > /tmp/cb.json <<'EOF'
|
|
1256
|
+
{"templateSourceId":"sfdc_internal__ServiceInsights","label":"Service Insights","name":"ServiceInsights_App","templateValues":{"dataSpaceApiName":{"dataSpaceName":"default"},"isTest":false}}
|
|
1257
|
+
EOF
|
|
1258
|
+
$ sf api request rest "$API_BASE/apps" \
|
|
1259
|
+
--target-org my-org --method POST --body '@/tmp/cb.json' --header "Content-Type: application/json"
|
|
1260
|
+
→ { id: "1zA…", applicationStatus: "InProgressStatus" }
|
|
1261
|
+
|
|
1262
|
+
# 5. Poll — is_inflight() keeps looping through NewStatus / QueuedStatus /
|
|
1263
|
+
# PendingStatus / AwaitingActivationStatus / InProgressStatus.
|
|
1264
|
+
$ # (see step 5 for the full loop with terminal + in-flight handling)
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
Total wall-clock: 8–15 min for Service Insights, 8–15 min for Sales Insights TN, 15–25 min for Employee / Service Agent Analytics, 5–10 min for C360 Semantic Model. `MAX_MINUTES=30` is the recommended cap.
|
|
1268
|
+
|
|
1269
|
+
## Anti-patterns (do NOT do these)
|
|
1270
|
+
|
|
1271
|
+
- **Don't install a non-allowlisted template.** Scope is Service Insights, Sales Insights TN, Employee Agent Analytics, Service Agent Analytics, and C360 Semantic Model. If the user asks for anything else (ITSM, Billing Central, C360InsightsStarterApp, etc.), refuse with the message under "Allowlist gate" and stop. Do NOT silently substitute "the nearest match" — that's a correctness bug.
|
|
1272
|
+
- **Don't offer alternatives outside the allowlist in the refusal.** The refusal message deliberately does not list "here's what we could install instead" — that would defeat the scope guard.
|
|
1273
|
+
- **Don't skip the allowlist gate on an id path.** If the user passed `0Nk…`, resolve the id to `.name` first and re-check the allowlist. Skipping this makes the id path an escape hatch around the whole restriction.
|
|
1274
|
+
- **Don't skip step 3 (readiness).** Templates without a `readiness.json` return `SuccessWithWarnings` and are safe to proceed; templates that DO ship `readiness.json` (SalesInsightsTN, ServiceInsights, Employee_Agent_Analytics, Service_Agent_Analytics, C360UnifiedSemanticModel, ...) return real failure reasons the user needs to see.
|
|
1275
|
+
- **Don't skip step 2b (C360 dependency detection).** If a template extends `C360SemanticModel_c360` and the C360 app isn't installed, readiness alone WILL NOT catch it — the install will fail late, after minutes of downstream provisioning. Refuse pre-install with the "install C360 first" message.
|
|
1276
|
+
- **Don't confuse `check_template_readiness` with chain-node walking.** `POST /readiness` only fires when `readiness.json` exists. Walking `create-chain.json` for `DataCloudRowCountReadiness` / `AccessExpressionReadiness` nodes and evaluating each against downstream Data Cloud APIs is a separate concern (MVP2). Don't imply this skill covers that.
|
|
1277
|
+
- **Don't retry `create_app` on `DUPLICATE_VALUE`.** apiName is taken — either delete the existing app first, or pick a new apiName.
|
|
1278
|
+
- **Don't call `delete_app` speculatively.** It's destructive on the installed app AND all workspace assets bound to it. Only call it as a rollback for a failed / unwanted install, and always re-confirm before deleting an app that already reached `SuccessStatus`.
|
|
1279
|
+
- **Don't paste the readiness body inline via `--body '{"…"}'`.** The sf-CLI's argument parsing mangles nested-quote escaping. Always write to a tempfile and pass `--body '@/path/to/file.json'`. The `@` prefix parses the file as JSON.
|
|
1280
|
+
- **Don't hardcode the `dataSpaceApiName` wire shape.** Wire rule C is template-dependent: dispatch on `variableType.type` from step 2 (`DataSpaceType` → object, `StringType` → bare string). Hardcoding either form breaks on the other family of templates.
|
|
1281
|
+
- **Don't POST `templateValues` containing `{{Variables.X.Y}}` placeholders.** Wire rule F — resolve them client-side before sending. Step 2's builder does this; if you hand-build the map, run the placeholder-resolution pass.
|
|
1282
|
+
- **Don't ignore `isTest` when the template declares it.** Wire rule E — unbound `isTest` silently returns `valueOnError` for every DMO readiness check and makes a green install look like a wall of "DMO not ready" failures.
|
|
1283
|
+
- **Don't break the poll on the first unknown status.** Wire rule D + the poll loop: only `Success*`/`Failed*` are terminal; anything ending in `InProgressStatus`/`PendingStatus`/`QueuedStatus`/`NewStatus`/`AwaitingActivationStatus` is in-flight and MUST keep the poll running. Defaulting to break-on-unknown truncates real installs when the platform ships a new in-flight variant.
|
|
1284
|
+
- **Don't skip the pre-POST baseline snapshot.** S2-G — if the create-app POST times out client-side, name-only recovery races against stale same-named records and concurrent operators. Snapshot `/apps?type=App` ids BEFORE the POST and diff after.
|
|
1285
|
+
- **Don't skip the `?type=App` filter on recovery lists.** S2-D — unfiltered `/apps` on some releases returns both apps and templates under one key; matching on `.name` alone lands on the wrong record.
|
|
1286
|
+
- **Don't skip the pre-POST `maxAppCount` check.** S2-I — hitting the cap surfaces as `MAXAPPSEXCEEDED` with no partial write, but you've already paid the client-timeout cost. Read `.maxAppCount` from the template metadata first and count installed apps of the same `templateSourceId`.
|
|
1287
|
+
- **Don't poll `GET /apps/{id}` forever after a DELETE that returned 204.** If the record still exists ~5 minutes after the DELETE (especially for stale multi-day-old FailedStatus records), the async `BulkDeleteAssets` chain is stuck — commonly on an active-flow blocker (`CannotDeleteException`). This is INVISIBLE on the wire. Route the user to **Setup → App Hub** to read the exact error, then to **Setup → Flows** to deactivate the blocker. See step 6's active-flow-blocker section.
|
|
1288
|
+
- **Don't confuse `maxAppCount` (org-wide) with the per-data-space cap.** Wire-verified 2026-08-25: `ServiceInsights.maxAppCount=5` but the template ALSO enforces 2-per-data-space, which the OAS does not publish. The pre-POST `maxAppCount` guard alone will happily let you POST into a data space that's already at its per-data-space cap; the platform then returns `applicationStatus=FailedStatus` with `Can't have more than [N] per dataspace [X] for this template`. Filter the count by data space when the template is known to enforce a per-data-space cap.
|
|
1289
|
+
|
|
1290
|
+
## Troubleshooting
|
|
1291
|
+
|
|
1292
|
+
| Symptom | Likely cause | Fix |
|
|
1293
|
+
|---|---|---|
|
|
1294
|
+
| Skill refuses with "not supported" / "not on the allowlist" | User requested a template outside the Service Insights / Sales Insights TN / Employee Agent Analytics / Service Agent Analytics / C360 Semantic Model allowlist | This is intended behavior. For other templates, use `sf api request rest` directly or a different skill. Do NOT re-invoke this one with the same input hoping for a different answer. |
|
|
1295
|
+
| Allowlist gate reports "AMBIGUOUS_AGENT_ANALYTICS" | User said "agent analytics" and both `Employee_Agent_Analytics` and `Service_Agent_Analytics` are candidates | Ask the caller which one. Re-invoke with the full apiName. |
|
|
1296
|
+
| Step 1 rejects a `0Nk…` id with "not on the allowlist" | User supplied a raw template id whose `.name` isn't one of the five allowlisted apiNames | Intended. Rewrite the request in terms of one of the allowlisted apiNames, or use a different skill. |
|
|
1297
|
+
| Step 2b refuses with "C360 app ... is NOT installed" | Selected template extends `C360SemanticModel_c360` but the C360 Semantic Model app is not installed on this org | Install C360 Semantic Model first via this skill (`install C360 Semantic Model`), wait for `SuccessStatus`, then re-invoke for the dependent template. |
|
|
1298
|
+
| Pre-POST `maxAppCount` guard refuses with "$N already installed" | Template caps per-org installs (e.g. SalesInsightsTN=1, ServiceInsights=5) and the org is at the cap | Delete an existing installation via step 6 (or `delete_app_by_name` if you've lost the id), wait for `APPNOTFOUND`, then retry. |
|
|
1299
|
+
| Poll loop keeps running on `QueuedStatus` / `PendingStatus` / `NewStatus` / `AwaitingActivationStatus` | Those ARE in-flight variants — the loop is behaving correctly | No action needed. Chain-heavy templates (Agent Analytics especially) commonly spend 30–90 s in each of these before landing on `InProgressStatus`. |
|
|
1300
|
+
| Poll loop breaks with "Unrecognized status '<X>' … Dumping and stopping" | The platform emitted a status whose suffix isn't in the terminal or in-flight allow-lists | Read the dumped payload — it's either a real new terminal state (add to the terminal `case`) or a new in-flight variant (add its suffix to `is_inflight`). Don't retry until the vocabulary is updated. |
|
|
1301
|
+
| Recovery via `delete_app_by_name` returns "Ambiguous: N apps named X" | Two records with the same `.name` (partial delete state, or the previous install kept its name across a re-run) | Refuses deliberately. Resolve manually via `GET $API_BASE/apps?type=App`, pick the correct id, delete by id. |
|
|
1302
|
+
| `list_tabnext_app_templates` returns `.templates: []` | Tableau Next not enabled OR user lacks visibility | Route user to Tableau Next enablement — out of scope for this skill |
|
|
1303
|
+
| Templates endpoint itself returns `NOT_FOUND` | Org doesn't have App-Framework Connect API rolled out at the discovered version | Confirm org is on a supported release (v67.0+); re-run the `GET /services/data` version discovery |
|
|
1304
|
+
| `GET /services/data` discovery fails or `$API_VERSION` ends up empty | sf-CLI not authenticated, org unreachable, or piping/`jq` step failed | Fix auth first (`sf org list --json`); then re-run the discovery. Do NOT fall back to hardcoded `v67.0` in a shipping build. |
|
|
1305
|
+
| `$API_VERSION` renders as `v68.0.0` (three-part, invalid) and every downstream call 404s | jq's `tonumber` on `"68.0"` returns the float `68.0` and the format `"v\(.).0"` renders it as `v68.0.0` | Add `\| floor` between `tonumber` and `max` in the jq pipeline. The sanity `case` on `$API_VERSION` catches it as a belt-and-braces. |
|
|
1306
|
+
| Step 2 jq raises `Cannot use null (null) as object key` | Used `.variables[]?` on the new response shape — `.variables` is an OBJECT MAP, not an array | Iterate with `.variables | to_entries[]` and read `.key` + `.value.defaultValue`. See step 2. |
|
|
1307
|
+
| Step 2 produced a `templateValues` containing optional variables (other than `isTest`) | Wrong client-side filter — `?required=true` isn't supported by the server | Filter with `select(.value.required == true or .key == "isTest")` in the jq pipeline. Required vars belong in `templateValues`; `isTest` is the one deliberate exception (wire rule E). |
|
|
1308
|
+
| `create_app` POST times out client-side (`Command timed out after 2m 0s`) with no `APP_ID` returned | sf-CLI blocks on the HTTP response; long-running installs exceed its 2-min default | Do NOT retry the POST — the original is still running server-side. Use the step 4 recovery block: `GET $API_BASE/apps`, filter on `.name == $APP_NAME` across `(.apps // .items // .records)`, take the first `.id`. Filtering on `templateSourceId` breaks when the POST used the apiName but the record stores the resolved id. |
|
|
1309
|
+
| Step 6 DELETE returns `SfError: No 'mode' found in 'body' entry` before hitting the server | sf-CLI's HTTP wrapper requires a body arg even on DELETE | Pass `--body '{}'` on the DELETE call. See step 6. |
|
|
1310
|
+
| Step 6 DELETE succeeds but subsequent `GET /apps/{id}` still returns the app | Server-side propagation is async | Poll `GET /apps/{id}` until it returns `APPNOTFOUND`; that can take a few minutes. |
|
|
1311
|
+
| Step 6 DELETE returned 204 but `GET /apps/{id}` still returns the app after 5+ minutes | The async `BulkDeleteAssets` chain is stuck — commonly on an **active flow version** the platform refuses to delete (`CannotDeleteException: You cannot delete this flow version because it is active`). Wire-invisible: neither `/apps/{id}` nor `/apps/{id}/activities` surfaces this. | Route the user to **Setup → App Hub**, expand the failed `BulkDeleteAssets` / `delete_tua` job to read the exact `301S…` asset id and reason, then to **Setup → Flows** to deactivate every active version of the flows the template installed. Re-issue the DELETE — it unwinds within 30–60 s. See step 6's active-flow-blocker section. |
|
|
1312
|
+
| User keeps retrying DELETE, each time getting 204 but the record persists | Same active-flow blocker as above. Retrying the DELETE does nothing — the server accepted the request; the async chain is what's failing | Stop retrying. Go to App Hub, read the failure reason, deactivate the flow(s), then delete once more. |
|
|
1313
|
+
| `create_app` returns `MAXAPPSEXCEEDED` | Template caps installed-apps-per-org (ITSM = 1) and you've hit the cap | Delete the existing app (step 6 + wait for `APPNOTFOUND`), then retry. |
|
|
1314
|
+
| `create_app` immediately returns `applicationStatus=FailedStatus` with `failureMessage: Can't have more than [N] per dataspace [X] for this template` | Template enforces a per-data-space cap that is DISTINCT from the org-wide `maxAppCount`. The pre-POST `maxAppCount` guard doesn't catch this because the count aggregates across data spaces. Wire-verified 2026-08-25 on ServiceInsights (per-data-space cap = 2). | Either (a) delete an existing install IN that data space (step 6 + wait for `APPNOTFOUND`, watching for the active-flow blocker), or (b) re-invoke with the data space variable pointing to a different data space. |
|
|
1315
|
+
| `check_template_readiness` returns `SERVER_UNAVAILABLE: Server too busy` | Transient platform load | Wait 5 s and retry the same call — it's idempotent. |
|
|
1316
|
+
| Variables endpoint returns `NOT_FOUND` | Template apiName / id typo, or template unpublished on this org | Re-list; ask user to pick from what's actually available |
|
|
1317
|
+
| `check_template_readiness` returns `Status: Failed` with DMO-missing reasons | Data Cloud DMO the template needs isn't mapped on the org | Route user to CRM connection setup / DMO mapping (out of scope) |
|
|
1318
|
+
| `check_template_readiness` returns `JSON_PARSER_ERROR: Value does not match expected type` | Body was bare `{}` or wrapped in the wrong key | Wire rule A — wrap as `{"templateValues": {...}}`. See step 3. |
|
|
1319
|
+
| `create_app` returns `JSON_PARSER_ERROR: Found unexpected object property: + userinputsforsingleclickdeploy` | Old MCP-tool schema leaks to wire | Wire rule B — rename to `templateValues`, or fall back to `sf api request rest`. |
|
|
1320
|
+
| Readiness or create returns `BAD_REQUEST: variable name [dataSpaceApiName] ... not of expected type [String]` | Template declares `dataSpaceApiName` as `StringType` but you sent the object form | Wire rule C — dispatch on `variableType.type`. `StringType` → bare `$DATA_SPACE_NAME` (resolved via wire rule G). |
|
|
1321
|
+
| Readiness or create returns `BAD_REQUEST: The variable name [dataSpaceApiName] with value [<X>] and type [String] is not of expected type [Object]` | Template declares `dataSpaceApiName` as `DataSpaceType` but you sent a bare string | Wire rule C — dispatch on `variableType.type`. `DataSpaceType` → object `{"dataSpaceName": $DATA_SPACE_NAME}` (resolved via wire rule G). |
|
|
1322
|
+
| Readiness returns HTTP 400 `TEMPLATEPARSEERROR: Data space [default] ... is unknown` or `Data space [<X>] ... is unknown` | Hardcoded `"default"` or a stale data space name that the org doesn't have | Wire rule G — resolve via `GET /ssot/data-spaces` first, then substitute the actual apiName. Never hardcode `"default"`. |
|
|
1323
|
+
| Readiness returns HTTP 400 `CDP not enabled` | Data Cloud isn't provisioned on the org | Route to admin — enable Data Cloud. Not fixable client-side; do NOT retry the install. |
|
|
1324
|
+
| Any App-Framework call (templates / variables / readiness / apps) returns HTTP 403 `ACCESS_DENIED` / `INSUFFICIENT_ACCESS` and the endpoint itself resolves | Caller is missing the `AppFrameworkManageTemplate` user permission | Have an admin assign a permission set carrying `AppFrameworkManageTemplate` ("Manage App Templates") to the caller, then retry. Distinct from the empty-templates-list 403, which is a feature-gate (Tableau Next not provisioned). |
|
|
1325
|
+
| Readiness or create returns HTTP 400 `The variable name [X] with value [N] must be one of the following values [5.0, 10.0, ...]` | Sent a bare integer (`360`) for a `NumberType` variable | Wire rule H — coerce every `NumberType` value to a decimal literal (`360.0`) before POSTing. Step 2's Pass 1b does this via Python (jq re-serializes floats as ints in most versions). |
|
|
1326
|
+
| Readiness returns `BAD_REQUEST: Object [{{Variables.X.Y}}] in variable [Z] is unknown, not supported, or is invalid` | A `{{Variables.X.Y}}` placeholder in a variable default wasn't resolved before POSTing | Wire rule F — run the placeholder-resolution pass from step 2 (walk the map, substitute against the same map, iterate to fixed point) before sending. |
|
|
1327
|
+
| Readiness reports every DMO "not ready" for DMOs the org demonstrably has | Template's readiness EL expression references an unbound variable (typically `Variables.isTest`); EL fails silently, falls back to each check's `valueOnError` (usually `-1`) | Wire rule E — bind `isTest: false` in `templateValues`. Step 2's builder does this automatically when the variable is declared. |
|
|
1328
|
+
| Readiness reports an EL `AccessExpressionReadiness` check like `ServiceIncludedAppAccess = false` | Org lacks the license / permission entitlement the template requires | Route to org admin — enable the missing license (e.g. Service Cloud / Service Intelligence). Not fixable client-side; do NOT retry the install. |
|
|
1329
|
+
| `create_app` returns `DUPLICATE_VALUE` | An app with this apiName already exists on the org | Pick a new apiName, or `delete_app` the existing one if user confirms |
|
|
1330
|
+
| `create_app` succeeds but subsequent `GET /apps/{id}` shows `applicationStatus: FailedStatus` | Async install chain hit a downstream failure | Read `.failureReasons[]`; if user gives up, `delete_app` to clean up |
|
|
1331
|
+
| `GET /apps/{id}` shows `status: null` | Status field is on `applicationStatus`, not `status` | Wire rule D — read `applicationStatus`. |
|
|
1332
|
+
| Any step returns `INVALID_SESSION_ID` / `Session expired` | sf-CLI credentials aged out | Ask user to re-run `sf org login web` and retry |
|
|
1333
|
+
| MCP tool call sits silent > 90s | project-codey / tableau-next-apps-mcp is unresponsive | Abandon the MCP call and re-run that step via `sf api request rest`. That's exactly why this skill is self-contained. |
|
|
1334
|
+
| `sf api request rest ... --body '{...}'` returns "url required" | sf-CLI arg parser mangled the inline body | Write body to a tempfile and use `--body '@/path/to/file.json'` — the `@` prefix parses the file as JSON. |
|
|
1335
|
+
|
|
1336
|
+
## Design rationale (why self-contained)
|
|
1337
|
+
|
|
1338
|
+
Two MCP wrappers (project-codey and tableau-next-apps-mcp/-beta) drifted from the App-Framework wire shape and returned opaque `JSON_PARSER_ERROR` / `BAD_REQUEST` errors when their schemas didn't match the OAS. On 2026-08-18 an `sfdc_internal__itsm` install on a live Data-Cloud-enabled ITSM sandbox succeeded in ~5.5 min via `sf api request rest` after project-codey MCP silently timed out for 30 min. Both wrappers were subsequently fixed, but the lesson stuck: **the skill owns the workflow end-to-end and treats MCP tools as an accelerator, not a dependency.**
|
|
1339
|
+
|
|
1340
|
+
This mirrors the pattern established in `skills/connect-headless-360-mcp/SKILL.md` and `skills/einstein-org-setup/SKILL.md`: platform-facing workflow skills that chain N already-existing tools/actions AND retain a canonical fallback path via `sf api request rest`.
|
|
1341
|
+
|
|
1342
|
+
## Related skills / references
|
|
1343
|
+
|
|
1344
|
+
- `skills/connect-headless-360-mcp/SKILL.md` — one-shot setup of the platform MCP server the tools run through. If tool calls fail with "no such tool" or "server not connected", this is the setup path.
|
|
1345
|
+
- `skills/mcp-server-development/SKILL.md` — how to add a new install preset tool (e.g. a future `wait_for_app_installed` MVP2 tool).
|
|
1346
|
+
- `skills/codey-gap-triage/SKILL.md` — routes gaps this skill can't fix (chain-node validation, per-template readiness overrides).
|
|
1347
|
+
- Memory: [[project_itsm_install_wire_shapes_2026_08_18]] — origin of wire rules A/B/C/D.
|
|
1348
|
+
- Memory: [[project_install_tabnext_app_skill_self_contained_2026_08_18]] — self-contained rewrite + wire rules E/F additions (`isTest` EL binding, `{{Variables.X.Y}}` placeholder resolution, `dataSpaceApiName` template-dependent shape).
|
|
1349
|
+
- Memory: [[project_codey_install_tabnext_app_skill_2026_08_17]] — MVP1 skill authoring history.
|
|
1350
|
+
- Memory: [[project_codey_mcp_5tool_install_mvp1_2026_08_17]] — the five underlying MCP tools this skill can optionally use.
|