Skip to content

Instantly share code, notes, and snippets.

@sumeet-bansal
Last active May 12, 2026 01:52
Show Gist options
  • Select an option

  • Save sumeet-bansal/b2a88e01ee99f0be73051d906247aa3a to your computer and use it in GitHub Desktop.

Select an option

Save sumeet-bansal/b2a88e01ee99f0be73051d906247aa3a to your computer and use it in GitHub Desktop.
Worker Interview Research skill for Traba's Worker Interview Program — schema gotchas, high-signal tables, report structure
name worker-interview-research
description Workflow for producing a research briefing on a Traba worker ahead of a Worker Interview Program call. Use when: (1) user asks for "deep research" / a "briefing" / "writeup" on a named worker, (2) user references the Worker Interview Program or asks who to prep for an interview, (3) user wants to understand a worker's experience on the app before a call. Covers: which prod tables to hit (and the gotchas), how to frame the worker as an archetype, how to scope interview angles to what's worth a 15-minute call, report structure.
version 1.6.1

Worker Interview Research

Cross-cutting concerns live elsewhere — read them first:

  • query-database — how to run prod queries against Postgres and BigQuery (bq prefix). Most of the tables below are Postgres; the BigQuery section adds the LLM-enriched and modeled layers Postgres doesn't have.
  • tex-preferences (rule) — title page, monochrome underlined links via \link{url}{text}, date formatting, compilation with tectonic. Do not use colorlinks=true — the rule is monochrome with \uline{}.
  • coda-mcp-gotchas — pulling pages from the Worker & Customer Research doc.
  • The cyrano subagent owns Slack delivery and the threaded-versioning DM pattern for drafts.

The framework lives in Coda

The Worker Interview Program is documented at coda://docs/EWmUjDXMMz (page IDs: section-HLk0LfQy8d Program, section-QSaEUjHbeA Interview Guide, section-Tw9iM5b4ti Worker Friendies). Pull the program page once per session for:

  • Segments (Flex vs Consistent) and lifecycle states (Unactivated → New → Underserved → Active → Fading → Dormant → Churned). Use these internally to inform the framing of the lede and interview angles. Don't print them in the report itself — they read as bureaucratic. See "Report structure" below.
  • Priority order: Active / Fading / Dormant first.
  • Universal questions (six, asked at the end of every interview): pick-a-shift drivers, non-shift pain points, non-shift help, other-agency exclusivity, referral triggers, exit triggers.

Prod schema gotchas

These will eat 4+ query attempts if you guess:

  • workers has no status column. Account state lives in account_status, joined by id = workerId (not by a workerId column). Fields: accountStatus, approvalStatusReason, terminatesAt, employmentType, phoneNumberStatus.
  • The strike table is strike, singular. Not strikes, not worker_strikes. Rich enum on reason: NO_CALL_NO_SHOW, LAST_MINUTE_NOTICE_SHIFT_CANCELLATION, VERY_SHORT_NOTICE_SHIFT_CANCELLATION, SHORT_NOTICE_SHIFT_CANCELLATION.
  • Past experience table is worker_past_experiences. Columns: businessName, positionName, lengthOfEmployment, agencyName (set if the gig was via Instawork etc.).
  • worker_attributes (the new V3 attribute system) is sparsely populated. For most workers it's empty. Use the legacy past_experiences table — fields category, profileField, value, opsLevel, aiLevel, resumeLevel — for the lifting / responsibilities / skills tags.
  • companies.industry doesn't exist. Use category (e.g. BEVERAGE_PRODUCTION).
  • shifts.supervisorId resolves to a User row, often the on-site customer manager (role SUPERVISOR, email at the customer's domain) — not a Traba ops user. This is the named human the worker actually deals with day-to-day. Worth surfacing in the report; reference them by first name in interview angles.

High-signal tables for narrative

Most of the report writes itself if you query these in order:

  1. worker_metrics — Tier, lifetime feedback score, perfect-shifts streak.
  2. work_preferenceshoursPerWeek, shiftConsistency (ONE_OFF vs recurring), transportation. Their stated preference is often misaligned with their actual behavior — call out the gap.
  3. worker_shifts × shifts × companies — the company breakdown by shift count / completed / canceled / no-show. Always join all three; raw worker_shifts.companyId doesn't exist (it's on shifts).
  4. connections — business-side FAVORITE / BLOCK / INELIGIBLE with operator note and initiatedBy. The who of customer love and customer rejection. initiatedBy is a free-text name field; cross-reference against the customer's user list to confirm it's an on-site person.
  5. company_worker_feedback — post-shift business→worker rating (rating 1–5) with optional note. Often has direct customer quotes that read as pull-quotes ("He left the production without letting anybody know…"). Source COMPANY is the in-app rating; other sources are ops-entered.
  6. worker_feedback — worker→business feedback. Reasons array (HELPFUL_SUPERVISORS, ENJOYABLE_ENVIRONMENT, etc.) plus optional info text. Sparse but high-signal.
  7. worker_churn_risk — AI-generated voluntary/involuntary scores per worker-company pair, plus prose fields (churnRiskWorkerFeedbackDetails, churnRiskCompanyFeedbackDetails, suggestedActionsDetails). The model has often already drafted half the narrative for you. involuntaryChurnRiskScore = 0 while voluntaryChurnRiskScore fluctuates means "the customer wants them, the worker is drifting" — a clean Active→Fading signal.
  8. strike — patterns over time matter more than counts. Cluster recent NCNS in months; if they all hit the same company, the relationship is the story.
  9. worker_shifts.cancellationReason and cancellationSource — the verbatim reason text is often gold ("I found a better paying work opportunity today"). Pull recent cancels with the reason and source separately; many cancels are OPS source (not the worker's fault) and shouldn't be counted against them.
  10. worker_applications × application_entities × shift_requests / roles × companies — every shift the worker has been invited to or applied to, even ones they didn't take. Often more revealing than the shifts they actually worked. See "Applications" below.
  11. worker_vetting_session + worker_vetting_phone_calls — AI-vetting outputs including summary (a paragraph of LLM-generated narrative from the call transcript), evalScore (0–1), and call status (VETTING_COMPLETE / MISSED_OUTBOUND / VETTING_INCOMPLETE). The summaries surface stated experience, pay expectations, commute, and red flags that don't appear anywhere else. See "Vetting calls" below.
  12. chat_messages where workerSenderId = workerId — the worker's own voice in the in-app chat. Filter by sender to read what the worker has actually said to ops or to customer supervisors. Often the most direct evidence of relationship dynamics and pain points. See "Chat messages" below.
  13. intercom_ticket — support tickets the worker has opened. ticketTypeName is the categorization ([Pay] Time dispute, [Gen] Other issues, etc.); ticketState shows whether it was resolved. The count and type matter even when descriptions are empty in our copy.
  14. worker_facts — heavily populated (200+ rows per active worker) with system-generated PREFERENCE rows: ROLES_V1, PAY_RATE_V1, SCHEDULE_DAY_AVAILABILITY_V1, COMMUTE_V1, SCHEDULE_CONSISTENCY_V1. The data field is JSON. The most-recent fact for each preference type reflects what was captured in the worker's most recent app/vetting interaction — so a worker whose pay range jumps from $16 fixed to $16–$25 range overnight is signaling pay flexibility on their latest vetting call. Compare today's facts to a month ago.
  15. worker_resumes + worker_resume_scores — uploaded resume plus AI-scored experience breakdown. priorEmployers is JSON with {employerName, tenureMonths} rows; per-role-type monthsOfExperience columns (forklift, lumper, machine operator, packer); fullTimeWorkHistoryScore and tenureAtPastEmployersScore. Often surfaces experience the worker never entered in their profile.

Manual actions vs bulk actions

The most important signal-quality distinction across all worker data:

  • Manual actions are high signal. A customer favoriting a worker, blocking a worker, or rating them in the app is a deliberate human act. So is an ops operator leaving a note on a connection or invalidating a strike. So is a worker writing a chat_messages row, opening an intercom_ticket, or canceling a shift with a typed reason. Every one of these is worth surfacing verbatim.
  • Bulk actions are low signal individually. Ops invites (OPS_INVITE), worker self-applications (TRABA_APP_WORKER_APPLY), shift invitations (shift_invitations), and outreach attempts (outreach_attempts) are mostly automated or spray-and-pray. A single record from any of these is meaningless. The volume of any single source means little.
  • Bulk actions become high signal as ratios over a meaningful sample. "Worker submitted 50 self-applications and 47 were screened ineligible" is signal — they're trying and getting rejected. "Worker has 88 ops invites at one customer and accepted zero of them" is signal — they're choosing not to. The narrative comes from the conversion or rejection rate, not the count.

Apply this filter when writing the report: every manual-action row deserves a quote or a sentence; every bulk-action count needs a paired conversion/rejection rate before it earns mention.

Applications

worker_applications is the table that tells you what work was offered to the worker, not what they did. For a worker stuck on one customer, this is often the most informative table in the database.

Join chain: worker_applications.applicationEntityIdapplication_entities → either shiftRequestId (then shift_requests.companyId and roleId) or roleId directly. Roles join to companies via roles.companyId. Build a COALESCE on the company name from both paths.

acquisitionSource is the most useful column. Distribution tells you who is driving the worker's pipeline:

  • OPS_INVITE — Traba ops actively inviting them. High volume = ops trying to diversify them.
  • BIZ_INVITE — customer asked for them by name. High volume from one customer = strong relationship.
  • TRABA_APP_WORKER_APPLY — the worker self-applied. The clearest signal of worker-driven intent.
  • DYNAMIC_SHIFT_FILLING_INVITE — backfill auto-invite.
  • WORKER_WAITLISTED — worker signed up for a waitlist.
  • VETTING_CAMPAIGN — came through an ops vetting campaign.

status and result tell you outcomes: COMPLETED_ELIGIBLE means they're cleared to take it, COMPLETED_INELIGIBLE means screened out, IN_PROGRESS means mid-process. A pile of COMPLETED_ELIGIBLE applications they never converted to shifts is its own signal — they had the option and didn't take it.

Per the manual-vs-bulk principle, raw application counts are not high signal — they're bulk pipeline output. What is high signal:

  • Acceptance/conversion rate per company. A worker eligible for 88 invites at one customer who has accepted exactly zero is real signal — they're being told no in some way, or saying no in some way. Compare invites-eligible to actual shifts worked at that customer.
  • Rejection rates over a sample. COMPLETED_INELIGIBLE plus result=ARCHIVED with resultReason. A worker submitting many applications that get screened out is having a bad app experience and is likely frustrated about it on the call. The ratio is the signal — "submits 50 apps, 47 ineligible" tells you something the count alone doesn't.
  • Recency clusters of self-applications. "168 self-apps to one new customer in the last three weeks" is different from "168 self-apps spread over a year." Tight clusters mean active intent right now; flat distributions mean they've always been clicking around.
  • The roles being invited or applied to. Pattern of role types tells you what work they're being matched against, which surfaces a question about whether the matching feels right.
  • Inviting customer history. If ops keeps inviting them to a customer they've been blocked at or rated 1/5 by, that's an ops-side observation to flag in the report (not a question to ask the worker).

Vetting calls

worker_vetting_phone_calls (joined through worker_vetting_session.workerId) is the AI-vetting layer. Two fields are gold:

  • summary — a paragraph of LLM-generated narrative from the call. Surfaces stated experience ("6 years of warehouse experience"), pay expectations ("$19/hour matching the offer"), commute ("carpools, 30-minute commute"), schedule preference, and "red flags" the model identified. Often reveals more than the worker's own profile.
  • evalScore — 0–1 confidence the worker is qualified. Useful but the summary text matters more.

Status counts also matter. Multiple MISSED_OUTBOUND records on the same day mean ops tried to reach them several times and failed — a responsiveness signal. VETTING_INCOMPLETE with summary "Insufficient transcript data" usually means a hangup or bad audio.

Data-quality gotcha: name transcription is unreliable. The AI agent regularly garbles the worker's name in the summary text — Matt Lutze became "Matthew Lott", Temeka Givens became "Sneaker Kids" / "Malika Kevin" / "Tamiko" across different calls. The substance of the summary is usually about the right worker (the join is reliable through workerVettingSessionIdworkerId), but don't quote the literal name from the transcript prose. Read past it.

Chat messages

Pull chat_messages where workerSenderId = workerId (note: workerId doesn't exist on this table; it's workerSenderId for worker-sent and companyUserSenderId for company-sent). The worker's own voice in the app is often the most direct evidence of the relationship.

Look for:

  • Pay disputes. Workers asking about not being paid, time entries not going through, clock-in failures, manual time entry. Often correlates with intercom_ticket records of type [Pay] Time dispute.
  • Names of supervisors not in our users table. Workers reference floor leads, shift managers, on-site contacts by first name ("Ryan", "Ant", "Uniquia"). These are real humans the worker deals with daily who don't appear anywhere else in the schema.
  • Constraints not in work_preferences. "no I got church tomorrow", "I have to leave my phone in the locker", schedule conflicts that won't show up in formal availability.
  • Tone. Friendly chitchat versus frustration versus pleading versus terse one-word responses. Tells you how they experience the platform.

Volume itself is a signal. Two messages over two years (Matt) means they don't use the chat layer. Twenty messages including a multi-day pay dispute (Temeka) means they actively run the relationship through the app — and any unresolved issues are visible there.

Worker facts and the recency-of-preferences trick

worker_facts is heavily populated and looks redundant on first read — hundreds of PREFERENCE rows that mostly say the same thing. The trick is comparing the most recent fact for each preference type to the older facts of the same type:

  • A worker whose PAY_RATE_V1 was min: 16 for months and is now min: 16, max: 25 told a vetting agent in the last few days they're open to higher pay.
  • A worker whose SCHEDULE_DAY_AVAILABILITY_V1 was M–F 8am–5pm and is now 7 days/week including overnight signaled wide-open availability on their last interaction.
  • A COMMUTE_V1 of 5 minutes from a worker whose home zip is 30 miles from the customer is either a misstatement or a transcript error from the vetting call — worth flagging.

Don't dump the fact list in the report; extract the change between recent and older facts as one or two sentences.

BigQuery: LLM-enriched and modeled layers

Postgres has the operational state. BigQuery has the analytics warehouse, including LLM-enriched columns and pre-joined marts that Postgres doesn't have. Always check BQ for these in addition to Postgres:

  • traba-app.marts.worker_shifts__cancel_categories — for every canceled worker-shift, an LLM has flagged the cancel reason against ~30 categories. Boolean flags include cancellation_category_health_flag, cancellation_category_sickness_flag, cancellation_category_transportation_issue_flag, cancellation_category_childcare_flag, cancellation_category_pay_errors_flag, cancellation_category_bad_supervisor_flag, cancellation_category_found_other_work_flag, cancellation_category_better_worker_match_flag, cancellation_category_too_far_flag, cancellation_category_extreme_temperature_flag, etc. The categorization is sparse (only fires when the cancel text or context maps cleanly), but each fire is a high-signal LLM judgment about why the worker missed. Group by category and surface in the report. Note: the LLM occasionally mis-categorizes (e.g. flags a medical cancel as "found other work") so treat each flag as a hypothesis, not a verdict — always cross-check against the Postgres cancellationReason text.

  • traba-app.marts.worker_shifts_vetting_enrichment — every worker-shift joined with vetting status, isHired (do they have a job placement at this customer?), isVetted, vetter (Traba employee who vetted them), wasBlocked flag (was an ineligible connection created right after this shift?), wasIneligibled, runningCompletes and runningHoursWorked (cumulative at that customer at the time of the shift), and shift_difficulty_score. Useful for understanding progression — a worker hitting wasBlocked = true on a shift tells you the block came from that specific shift, not nowhere.

  • traba-app.marts.placements — surrogate-key (worker_id, company_id, region_id) placements with placement_first_* columns describing how the relationship started (invite type, signup status, whether it was recurring/infinite schedule, role family). Tells you the origin story of a worker-customer pair.

  • traba-app.metrics.placements_by_day__churn — per-placement churn timing.

  • traba-app.metrics.monthly__worker_ltv — modeled lifetime value per worker.

  • traba-app.intercom.conversation_part_history — full Intercom message bodies. Postgres intercom_ticket only has metadata; BQ has the actual conversation text. Use this when ticket counts in Postgres look interesting and you want to read what the worker actually wrote.

  • traba-app.freshdesk.conversation — Freshdesk tickets with body text.

  • traba-app.worker_app.* — 320+ Segment-tracked event tables for the worker mobile app (every tap, screen-load, application step). Use sparingly — naming is consistent (application_step_<name>_clicked, shift_<event>_view) but the breadth is overwhelming. Worth a focused query when investigating a specific app-experience hypothesis ("did they ever land on the help screen for pay disputes?") rather than a broad scan.

  • traba-app.firestore_export_prod.timesheets_raw_latest — supervisor-app timekeeping events (clock-in, clock-out, breaks). Postgres has the resolved worker_shift.timeWorked; BQ has the raw events behind it. Useful when investigating clock-in failures or pay disputes (Temeka-style).

Casing changes between sources: in BQ marts, columns are snake_case (worker_id, shift_date); in src_pg, they preserve Postgres camelCase (workerId). Reference tables with backticks: `traba-app.marts.worker_shifts__cancel_categories`.

Tables that are mostly noise

A few tables look promising and aren't:

  • outreach_attempts (1000+ rows per worker) — mostly automated dynamic-fill outreach with little narrative content. Skip unless you have a specific question about responsiveness windows.
  • worker_communication (3000+ rows per worker) — combined SMS / push / email / AI-call log. Volume is huge and most of it is automated. Consider filtering by source/medium if you have a specific question.
  • shift_invitations (hundreds per worker) — direct shift invites. Counts mirror worker_applications and don't usually add narrative beyond it.
  • worker_uploads — typically zero rows for active workers. Workers don't upload media in the normal flow.

The slot-demand frame

For Consistent-segment workers, look at shifts.slotsRequested over time at their dominant company:

  • 1 slot/day, 5 days/week → the worker is the customer's outsourced employee. Their no-show is a 100% staffing failure.
  • Many slots/day → they're one of N interchangeable workers. Different relationship dynamic; the customer can absorb a no-show.

State this explicitly ("X is a 1-slotter at Y") because it changes the empathy frame for the call.

Cancel reasons need disaggregation

A worker with 141 canceled shifts is not necessarily flaky. Group by cancellationSource:

  • WORKER — their cancel. Pull the verbatim cancellationReason.
  • BUSINESS — customer canceled (weather, demand drop, "biz req").
  • OPS — Traba ops canceled. Often a mass placement teardown that hits one worker hard; flag and exclude from any "this worker is unreliable" framing.
  • SYSTEM — auto-cancellation (e.g., expired pending shift).

The Legendairy mass-cancel pattern — 21 ops cancellations across consecutive days at one company — looks awful in a count and is meaningless in narrative. Always disaggregate.

Ops console URL patterns

For the report:

  • Worker: https://console.traba.work/workers/<workerId>
  • Company: https://console.traba.work/companies/<companyId>

(The ops console used to live at ops.traba.work; the canonical domain is now console.traba.work. Use the new domain in all new reports.)

Per tex-preferences, render as underlined \link{url}{Name} — never paste raw IDs in the prose. Define \workerlink and \colink macros at the top of the report.

Report structure: one-pager + appendices

The user wants a single readable page about the worker, with detail in appendices. Front-load substance, not stats:

Page 1 (the one-pager):

  1. Lede paragraph. Open with the tension or insight, not facts the snapshot is about to repeat. The job of the lede is to make the reader want to take the call. Use the lifecycle insight (e.g., "the customer wants him; the worker is starting to drift") without naming the program framework — drop "Consistent-segment", "Active→Fading", "P0 priority". Those are internal taxonomy and read as bureaucratic in the brief.
  2. Snapshot table. Two columns of the load-bearing facts: profile (with ops-console link), home base, primary account + supervisor, schedule, pay, tenure, acquisition. Don't include rows the lede already covered (no "Archetype" row, no "Segment/state" row).
  3. What their week actually looks like. 3–4 bullets translating the data into lived experience. For workers on a recurring placement: emphasize that "the app is a timeclock + paystub" — the marketplace framing doesn't apply, which changes how the standard questions land.
  4. Two questions worth a short call. The user's instruction is exclusive, not comprehensive. A 15-min call has room for two worker-specific opens before the universal six. Pick angles that test the central hypothesis. Don't list every interesting thread; everything else is appendix material.
  5. One short practical line — worker-specific intel only. The principle: include only what the caller wouldn't already have from being on the team. Phone number, language preference if non-default, anything genuinely unusual about how to reach them. Skip anything procedural — "text first per the SOP", "don't call during their shift window", "twice/day", "24h escalation". The caller's job is the call; you're not teaching them how to do it. Often this whole line is unnecessary; only add it if there's truly worker-specific intel (e.g., "Spanish-preferred", "best window 8--10am ET because of childcare").

Appendices: reliability deep-dive (NCNS clusters, churn-risk model output), customer-side signals (favorites/blocks/worst review with quotes), companies-worked-at table, customer ratings chronological, strike ledger, worker's own feedback to Traba (verbatim), past experience.

The threshold for "worth surfacing on page 1" is high: it has to change the call's opening question or shift the empathy frame. Everything else goes to an appendix.

Lede patterns: insight, not facts

The lede paragraph is where iterations get lost. Common failure: it ends up restating the snapshot in prose ("Matt is a 31-month veteran in Austin, who works at X for Y hours…"). Instead, lead with the tension that makes the worker interesting:

  • "By the data, X is both the most reliable and least reliable worker on this list — depending on which months you look at."
  • "X has worked Y 154 times in the last year. Y still wants more shifts from him; X has been quietly missing more of them."
  • "On paper, X is a textbook Active worker. In practice, he's started skipping the same shift type at the same customer four months in a row."

The insight should be earned from the data, not a label imported from the program framework. If you find yourself writing "is a P0 priority because the segmentation says…", delete and rewrite from the data.

Archetype framing (use sparingly)

A one-line archetype label can be useful in the snapshot or interview-prep section. Don't make it a section heading or a separate row in the snapshot table — that reads as overformal. Examples:

  • "The Dedicated 1-Slot Anchor" — recurring worker as customer's full-time employee.
  • "The Multi-Account Bouncer" — no dominant account, high recency across many.
  • "The Roster Fade" — was anchored, has drifted, data hasn't acknowledged it yet.
  • "The Pre-Quit" — active by completion rate, but cancel/no-show pattern says they're already gone.

If the lede paragraph already conveys the archetype implicitly, drop the label.

Recency weighting

Front-load recent activity (last 30–90 days). Older history goes to appendices. The caller is talking to the worker now — what happened twelve months ago is context, what happened last week is the conversation. Specific applications:

  • The "what their week looks like" bullets should describe their current schedule, not a lifetime average.
  • Cancel/no-show framing should lead with the most recent cluster, not the lifetime count.
  • Customer relationship signals: most recent rating and most recent favorite/block matter more than the lifetime ledger.
  • The companies-worked-at table belongs in an appendix, not on page 1, unless the worker bounces across many.

Don't over-interpret no-shows

NCNS counts read like a smoking gun and usually aren't. Before framing them as a drift signal, sanity-check three things:

  1. Are they concentrated at one customer? For a worker with one dominant account, "all NCNS at X" is tautological — they have no other shifts to no-show on. The interesting question is whether the rate at that customer is changing over time, not the absolute count.
  2. Are they clustered or distributed? A 4-in-4-days cluster reads like a life event (illness, family, transit failure). Evenly distributed misses read more like erosion.
  3. Has the strike been invalidated or proven? Look at strike.proven and strike.invalidatedBy. System-invalidated strikes (e.g., shift later canceled by ops) shouldn't count toward the narrative.

Also: cluster + recent return to perfect attendance ≠ drift. Often the worker handled whatever it was and is back. State the data; let the call test the meaning. Avoid words like "drifting", "fading", "pre-quit" in the report — those are interpretations the user is going to push back on, and they pre-empt the call's job.

Sparse customer engagement (observed once, not yet validated)

Temeka Givens had 66 completed shifts at QuickBox Fulfillment with zero company_worker_feedback rows. That looked like a productive interview thread — "are you treated as faceless labor by this customer?" — but it's n=1 in this skill so far. Worth probing on the call when you see it; worth building a base rate before turning it into a rule. If you see a second case, validate or revise.

Title block formatting

Per tex-preferences, links are underlined via \uline{}. Don't underline a link inside a \Large+ bold title — the descenders collide with the underline and look broken. Drop the link from the title; put it in the snapshot table instead as "Profile: \workerlink{Open in ops console}".

Spacing gotcha: a negative \vspace immediately before a \hrule will pull the rule up into the subtitle line. Use positive spacing (\vspace{0.5em} before, \vspace{0.7em} after), or skip the rule entirely and rely on visual hierarchy from the type sizes. The latter is usually cleaner for a one-pager.

Delivery

Send via cyrano with the threaded-versioning DM pattern (parent message with summary blurb, file uploads as thread replies labeled v1/v2/...). Expect ~3–5 iterations: framing, length, formality, link rendering, lede dedup. The data work is usually right by v1; the form is what gets refined.

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