Skip to content

feat(workiz): add workiz#1441

Open
eboziev wants to merge 8 commits into
mvanhorn:mainfrom
eboziev:feat/workiz
Open

feat(workiz): add workiz#1441
eboziev wants to merge 8 commits into
mvanhorn:mainfrom
eboziev:feat/workiz

Conversation

@eboziev

@eboziev eboziev commented Jul 4, 2026

Copy link
Copy Markdown

workiz

Every Workiz dispatch and pipeline workflow, plus crew utilization, revenue, and conversion views no other Workiz tool has.

API: workiz | Category: sales-and-crm | Press version: 4.24.0
Spec: Not specified (hand-authored internal YAML spec; no official OpenAPI/Swagger spec is published by Workiz — see research brief for ground-truth sourcing from 4 independent SDKs)

Publication Path

New print

CLI Shape

$ workiz-pp-cli --help
Workiz CLI — Every Workiz dispatch and pipeline workflow, plus crew utilization, revenue, and conversion views no other Workiz tool …

Highlights (not in the official API docs):
  • team bottleneck --week   See per-crew scheduled load and catch double-bookings or time-off conflicts before they become no-shows.
  • lead funnel   See which lead sources actually turn into paid jobs, with conversion rate and average resulting job value per source.
  • job revenue   Roll up total and outstanding job value by lead source and job status.
  • job audit   Find jobs, leads, and clients missing phone, email, amount, or crew fields that would block a downstream billing push.
  • digest   See everything new or changed across jobs and leads since your last check, grouped by entity.
  • job search   Search job notes, lead notes, and comments for free text across your entire synced history.

Agent mode: add --agent to any command for JSON output + non-interactive mode.
Health check: run 'workiz-pp-cli doctor' to verify auth and connectivity.
See README.md or the bundled SKILL.md for recipes.

Usage:
  workiz-pp-cli [command]

Available Commands:
  agent-context Emit structured JSON describing this CLI for agents
  auth          Manage authentication for Workiz
  completion    Generate the autocompletion script for the specified shell
  customer      Get and create customer
  digest        See everything new or changed across jobs and leads since your last check, grouped by entity.
  doctor        Check CLI health
  feedback      Record feedback about this CLI (local by default; upstream opt-in)
  help          Help about any command
  import        Import data from JSONL file via API create/upsert calls
  job           Scheduled service calls (jobs)
  lead          List, get, create, and update lead
  profile       Named sets of flags saved for reuse
  search        Full-text search across synced data or live API
  sync          Sync API data to local SQLite for offline search and analysis
  team          List and get team
  timeoff       List and get timeoff
  version       Print version
  which         Find the command that implements a capability
  workflow      Compound workflows that combine multiple API operations

Novel Commands

Command Name Description
team bottleneck --week Crew utilization & conflict bottleneck view See per-crew scheduled load and catch double-bookings or time-off conflicts before they become no-shows.
lead funnel Lead-to-job conversion funnel See which lead sources actually turn into paid jobs, with conversion rate and average resulting job value per source.
job revenue Revenue pipeline by source/status Roll up total and outstanding job value by lead source and job status.
job audit Missing-data / billing-readiness audit Find jobs, leads, and clients missing phone, email, amount, or crew fields that would block a downstream billing push.
digest Since change digest See everything new or changed across jobs and leads since your last check, grouped by entity.
job search Full-text search across notes and comments Search job notes, lead notes, and comments for free text across your entire synced history.

What This CLI Does

Workiz has no CLI, MCP server, or agent-native tool today — every existing integration is a thin polling script or a hand-rolled SDK wrapper with zero cross-entity intelligence. This CLI absorbs every job/lead/client/team/time-off operation from the SDK ecosystem (18 endpoints across 5 resources, matching and beating 4 independent open-source SDKs plus Pipedream's official integration), then adds local joins across synced data to answer questions the live API simply can't: who's overbooked this week, which lead sources convert, and what changed since you last checked.

Workiz's auth is non-standard (API token embedded in the URL path, API secret in the POST body) — handled via two documented customizations (see .printing-press-patches/) rather than the generator's standard header/query auth injection.

Manuscripts

Validation Results

Check Result
Manifest PASS
Transcendence PASS
Phase 5 PASS (skip marker: auth-unavailable)
go mod tidy PASS
govulncheck (this CLI only, reachable findings) PASS
go vet PASS
go build PASS
--help PASS
--version PASS
verify-skill PASS
Manuscripts PRESENT
Scorecard 92/100, Grade A
Shipcheck (6-leg umbrella) PASS (6/6)

Publish Live Gate

Skipped with explicit reason: auth-unavailable: no Workiz API credentials provided by the user for this session. Workiz requires a Developer API add-on token + secret pair that the operator did not have available. Everything else was verified via dry-run, mock-mode shipcheck, and hand-checked behavioral testing against a synthetic SQLite fixture standing in for real synced data (see Build Log for the exact assertions).

Notable customizations

Two hand-authored patches (.printing-press-patches/) work around Workiz's non-standard auth shape, which the generator has no built-in mode for:

  • workiz-token-in-path-auth.json — API token folded into the base URL as a path segment at request-build time, not persisted, after an earlier version accidentally persisted and doubled the token across auth set-token rotations.
  • workiz-auth-secret-body-injection.json — API secret auto-injected into every write request's JSON body, with --dry-run masking to match the existing URL/header masking.

A parallel 3-agent code review (correctness/security/maintainability) caught and fixed 2 credential-leak bugs and 1 critical token-persistence bug before this PR was opened; see the Phase 4.95 findings report linked above for full detail.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for 80c604ff86fa497ef48a158e03253b178d6eba0c after 184s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces the Workiz CLI integration for the printing-press library, covering all five Workiz resources (job, lead, customer, team, time-off) plus six novel analytics commands not present in any existing Workiz SDK: team bottleneck, lead funnel, job revenue, job audit, digest, and job search. Two non-standard auth patches handle Workiz's token-in-URL-path and secret-in-body requirements, and a parallel 3-agent code review addressed credential-leak and token-persistence bugs before this PR was opened.

  • Auth layer: EffectiveBaseURL() injects the token as a path segment at call time (never persisted to BaseURL); doInternal() auto-injects auth_secret into every map-shaped write body and masks it in --dry-run output; auth set-secret persists the secret to disk so write credentials survive shell restarts.
  • Novel analytics: all six commands read from the local SQLite mirror; filterLeadsSince, matchLeadToJob, and resolveTimeOffOwner carry regression tests covering the bugs surfaced and fixed across multiple review rounds.
  • One open bug: job audit passes "job,lead" to checkNovelMirror but also loads the customer table — the first-time sync hint is missing "customer", so users who follow it will run the audit with an empty customer table and see no customer-level findings.

Confidence Score: 4/5

Safe to merge with one correction: the job audit command needs its sync-hint resources string updated from "job,lead" to "job,lead,customer" to avoid guiding first-time users toward an incomplete mirror that produces zero customer findings.

The bulk of the code — auth patches, novel analytics commands, and the multi-round fixes to credential leaks, lead funnel filtering, and time-off conflict detection — is well-implemented and regression-tested. One concrete gap remains: job_audit.go advertises "job,lead" as the resources to sync but also reads the customer table, so users who follow the first-time sync hint will silently miss all customer-level billing-readiness issues. All other changed files reviewed cleanly.

library/sales-and-crm/workiz/internal/cli/job_audit.go — the checkNovelMirror resources string needs to include "customer".

Important Files Changed

Filename Overview
library/sales-and-crm/workiz/internal/cli/job_audit.go Sync-hint resources string is "job,lead" but command also loads customers; first-time users following the hint will run the audit with empty customer data and miss all customer-level billing-readiness issues.
library/sales-and-crm/workiz/internal/cli/auth.go auth set-secret command added; placeholder labels fixed; both auth setup and auth status correctly reference both credentials.
library/sales-and-crm/workiz/internal/config/config.go AuthHeader() fixed to require token (no secret-only fallback); EffectiveBaseURL() cleanly computes path-embedded token; SaveSecret() persists secret to disk.
library/sales-and-crm/workiz/internal/cli/lead_funnel.go filterLeadsSince correctly excludes undated leads; matchLeadToJob now prefers earliest-dated candidate and correctly handles undated-first iteration order.
library/sales-and-crm/workiz/internal/cli/team_bottleneck.go Time-off conflict detection fixed via email-based username-to-display-name resolution; double-booking detection via consecutive-pair check on sorted jobs is correct.
library/sales-and-crm/workiz/internal/client/client.go auth_secret injected into every map-shaped write body; dry-run masking covers the body field; cross-host redirect drops auth header correctly.
library/sales-and-crm/workiz/internal/cli/novel_shared.go parseWorkizTime handles null/empty correctly; flexibleMoney/flexibleID absorb Workiz wire-shape variations; in-place filterLeadsSince pattern is safe.
library/sales-and-crm/workiz/internal/store/store.go All five resource tables (job, lead, customer, team, timeoff) created unconditionally in migration; backfillColumns handles schema evolution idempotently.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant CLI as workiz-pp-cli
    participant Config
    participant Client
    participant WorkizAPI as Workiz API
    participant SQLite

    User->>CLI: workiz-pp-cli auth set-token TOKEN
    CLI->>Config: SaveCredential(token)
    Config-->>CLI: config.toml written (api_token)

    User->>CLI: workiz-pp-cli auth set-secret SECRET
    CLI->>Config: SaveSecret(secret)
    Config-->>CLI: config.toml written (api_secret)

    User->>CLI: workiz-pp-cli sync
    CLI->>Config: "Load() → EffectiveBaseURL() = base/TOKEN"
    CLI->>Client: New(cfg)
    Client->>WorkizAPI: GET /TOKEN/job/all/ (Authorization: TOKEN)
    WorkizAPI-->>Client: "[{job data}]"
    Client->>SQLite: UpsertBatch(job, lead, customer, team, timeoff)

    User->>CLI: workiz-pp-cli job create --first-name Jane ...
    CLI->>Client: Post("/TOKEN/job/create/", body)
    Note over Client: doInternal() injects auth_secret into body
    Client->>WorkizAPI: "POST /TOKEN/job/create/ {..., auth_secret: SECRET}"
    WorkizAPI-->>Client: "{data: {UUID: "..."}}"
    Client-->>CLI: result JSON

    User->>CLI: workiz-pp-cli lead funnel --since 30d
    CLI->>SQLite: loadLeads() + loadJobs()
    SQLite-->>CLI: leads[], jobs[]
    Note over CLI: filterLeadsSince → matchLeadToJob → aggregate by source
    CLI-->>User: "source | leads | converted | rate | avg_job_value"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant CLI as workiz-pp-cli
    participant Config
    participant Client
    participant WorkizAPI as Workiz API
    participant SQLite

    User->>CLI: workiz-pp-cli auth set-token TOKEN
    CLI->>Config: SaveCredential(token)
    Config-->>CLI: config.toml written (api_token)

    User->>CLI: workiz-pp-cli auth set-secret SECRET
    CLI->>Config: SaveSecret(secret)
    Config-->>CLI: config.toml written (api_secret)

    User->>CLI: workiz-pp-cli sync
    CLI->>Config: "Load() → EffectiveBaseURL() = base/TOKEN"
    CLI->>Client: New(cfg)
    Client->>WorkizAPI: GET /TOKEN/job/all/ (Authorization: TOKEN)
    WorkizAPI-->>Client: "[{job data}]"
    Client->>SQLite: UpsertBatch(job, lead, customer, team, timeoff)

    User->>CLI: workiz-pp-cli job create --first-name Jane ...
    CLI->>Client: Post("/TOKEN/job/create/", body)
    Note over Client: doInternal() injects auth_secret into body
    Client->>WorkizAPI: "POST /TOKEN/job/create/ {..., auth_secret: SECRET}"
    WorkizAPI-->>Client: "{data: {UUID: "..."}}"
    Client-->>CLI: result JSON

    User->>CLI: workiz-pp-cli lead funnel --since 30d
    CLI->>SQLite: loadLeads() + loadJobs()
    SQLite-->>CLI: leads[], jobs[]
    Note over CLI: filterLeadsSince → matchLeadToJob → aggregate by source
    CLI-->>User: "source | leads | converted | rate | avg_job_value"
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor Fix All in Conductor

Reviews (8): Last reviewed commit: "fix(workiz): add auth set-secret so writ..." | Re-trigger Greptile

Comment thread library/sales-and-crm/workiz/internal/config/config.go
Comment thread library/sales-and-crm/workiz/internal/cli/auth.go
Comment thread library/sales-and-crm/workiz/internal/cli/lead_funnel.go Outdated
- AuthHeader() no longer falls back to a secret-only credential: the
  secret is only ever a POST-body field, so a config with just
  WORKIZ_API_SECRET set now correctly reports unauthenticated instead
  of passing validateCachedRequestAuth while every real request 404s.
- Fixed misleading auth setup/status placeholder text that told users
  to paste their token into the WORKIZ_API_SECRET slot.
- lead_funnel's lead-to-job matching no longer silently disables its
  chronology guard when a lead's CreatedDate is unparseable; extracted
  matchLeadToJob() picks deterministically (earliest-created match)
  instead of depending on iteration order.
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for 5cbf61981a508db5ae75521509ca0421294bedba after 185s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

…in matchLeadToJob

Greptile's second review pass caught a residual bug in the previous
fix: when the first contact-matching job had no parseable CreatedDate,
bestCreated stayed at the zero time.Time value and no later dated job
could satisfy jobCreated.Before(zero) to displace it, so the undated
match would win permanently. Selection now explicitly prefers any
dated candidate over an undated one, then earliest-created among dated
candidates, falling back to undated only when nothing dated exists.
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for 9e4f00c560b13b48b2cf6cc0e84df29e935d50f8 after 183s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

- store.go: job/lead sync silently dropped every real record. Workiz
  returns a fully-uppercase "UUID" field, but the generic ID-fallback
  matcher only tries id/ID/uuid/Uuid case variants, never all-caps.
  Added an explicit resourceIDFieldOverrides entry so job/lead resolve
  their primary key directly.
- novel_shared.go: wzJob.JobTotalPrice/JobAmountDue were declared as
  string, but live responses return JSON numbers (int or float) —
  json.Unmarshal fails the whole struct on a single field type
  mismatch, so every real job row was silently dropped from
  loadJobs. Introduced flexibleMoney to accept both wire shapes.
  Also: wzComments only handled empty-string/array-of-objects shapes;
  live Comments are plain non-empty strings, so real comments were
  silently lost. wzLead.LeadSource was tagged "LeadSource", but every
  live lead reports its source under "JobSource" instead — lead
  funnel's group-by-source always fell through to (unknown).
- lead_funnel.go: the job-predates-lead chronology guard was too
  strict for real data. Workiz's own "AI Call" intake integration
  creates the job record 2-3 seconds before the lead record for the
  same contact, so a strict jobCreated >= leadCreated check rejected
  every real conversion from that source. Added a 10-minute grace
  window.

Verified end-to-end against a real Workiz account: sync now populates
all 4 resources cleanly (0 warnings, was previously dropping every
job/lead), and job revenue/team bottleneck/lead funnel/job audit all
produce correct output against real data (lead funnel now finds all 6
real conversions at 40% rate, was 0% for every source before this fix).
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for 2a8afd527ace72fd91c6190e0d4887f8bf9a3447 after 185s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

Greptile flagged two missing items per AGENTS.md's canonical PR shape
contract: dogfood-results.json and a spec_source field. Both trace to
the same root cause — dogfood-results.json (which itself carries
spec_source: "bundled") was present in the local library copy but
was dropped when the CLI was first copied into the publish repo.
Restored from the local library.
@eboziev

eboziev commented Jul 4, 2026

Copy link
Copy Markdown
Author

Re: missing dogfood-results.json / spec_source — both trace to one root cause: dogfood-results.json (which itself carries spec_source: "bundled") existed in the local library copy but was dropped when first copying into the publish repo. Restored in 9a3ca50.

For clarity, .printing-press.json's own spec_source field is correctly null here per this repo's own AGENTS.md: "spec_source — internal-yaml, graphql-sdl, or a path (for plan-driven runs); absent for --spec runs". This CLI was generated via --spec workiz-spec.yaml (hand-authored internal YAML, no official OpenAPI spec exists for Workiz), so an absent field there is expected, not a gap.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for 9a3ca50cac3ceeabbbf69143d9716b32ffd0d458 after 186s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

Comment thread library/sales-and-crm/workiz/internal/cli/lead_funnel.go Outdated
Greptile finding: the condition ok && t.Before(cutoff) short-circuited
to false for leads with an empty/unparseable CreatedDate, so undated
leads always passed through regardless of --since, inflating
per-source counts and skewing conversion rates for a scoped query.
Extracted filterLeadsSince() and changed the guard to !ok ||
t.Before(cutoff) so both cases are excluded. Verified against live
data: --since 7d now correctly returns 17/53 leads instead of leaking
all 53.
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for a375af30d32f9647075168f88efd78953b49c4a2 after 184s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

Comment thread library/sales-and-crm/workiz/internal/cli/team_bottleneck.go
Greptile finding: TimeOff.UserName is a login/username, not the
display name job.Team[].Name reports (confirmed by the community PHP
SDK, which calls the TimeOff lookup parameter "username" — a separate
identifier space). A bare string comparison between the two never
matched, so type:"time_off_conflict" entries were unreachable in
practice despite the detection logic looking correct.

Added buildUsernameToDisplayName()/resolveTimeOffOwner(), which resolve
a TimeOff record's UserName to a display name via the synced team
roster's email field (matching full email or its local-part,
case-insensitive) — email is the only field shared between the timeoff
and team resources. Falls back to the raw UserName when no roster
match is found, so a time-off record is never silently dropped.

Could not verify against live data (the test account has no recorded
time-off), so this is a best-effort fix based on the SDK-documented
semantics of the UserName field, not an empirically-confirmed match.
Added regression tests for the email/local-part/no-match cases.
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for f369ab021f580709a4f5cff02d8036c2cc2dcbdd after 186s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

Comment thread library/sales-and-crm/workiz/internal/cli/auth.go
…aled

set-token only ever persisted WorkizApiToken. WorkizApiSecret could only
come from WORKIZ_API_SECRET, so a new shell without that var exported
sent every write request to Workiz missing the required auth_secret body
field, with no CLI-side indication of why.

Adds Config.SaveSecret and `auth set-secret <secret>`, mirroring the
existing set-token command, plus a regression test covering persistence
and rotation.

Greptile finding: PR mvanhorn#1441, internal/cli/auth.go:144
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@greptileai review

Auto-nudge from greptile-policy-gate.yml because no Greptile Review check appeared for b193b48e118dfad22e875a1fa6ee68a48f463e7a after 184s. This usually means the PR is over Greptile auto-review size cap; manual triggers bypass it.

}
ctx := cmd.Context()
var bail bool
if dbPath, bail = checkNovelMirror(cmd, flags, dbPath, "job,lead", []auditIssue{}); bail {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Sync hint omits customer resource, silently producing incomplete audit results

checkNovelMirror is called with "job,lead" but job audit also loads the customer table. The migration always creates the customer table so there is no runtime error — but if a first-time user follows the displayed hint (workiz-pp-cli sync --resources job,lead), the customer table remains empty. Every subsequent job audit run will report zero customer-level issues regardless of actual data, making the customer section of the audit a silent false-negative. The resources string on this line should be "job,lead,customer".

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant