Feature breakdown · built with CraftHQ
Bulk import from a spreadsheet
Whole-team time at this build's pace
2 people incl. the product owner, requirements to merge
Estimate at this build's pace, not a measurement of this feature
Update hundreds of existing records from one sheet, matched on a stable code. A staff member can update only the records they own, and changing the owner or reviewer is refused on the server.
How long a feature this size usually takes
Whole-team time — this build vs a typical team
~25 FP · FP × 6.8 to 9.4 h (ISBSG 2023)
Estimate at this build's pace, not a measurement of this feature
Both figures are whole-team time over the whole lifecycle, so they compare like for like. Ours is this build's pace — about 0.8 hours per function point across the whole build (two people, including the product owner, with requirements and review time). The industry range is an ISBSG project-level figure applied to one feature, so it is indicative.
Workflow
How it moved through the workflow
Select a stage. The build shipped, so every stage is complete; a person approved the merge.
Request
Update hundreds of existing records from one sheet; assignment stays an admin decision, and a staff member updates only the records they own.
Questions
Four decisions went to the business before building: the match key, bad or self-assigned owners and reviewers, records in a locked stage, and whether a staff import may reassign.
Approved
The product owner signs off the breakdown by moving the story into the approved state, and the build runs against that reference.
Definition of ready
Building
CraftHQ's build agents write the code and the tests together, run as a loop; a failing check halts the work until it passes.
Checks
Three checks run on every pull request: one on the product owner's machine as CraftHQ builds, one on the engineer's machine before review, and a code-quality scan in CI. Merging is gated by the CI check passing and a person's approval, enforced by branch protection.
Review
An engineer reviews the pull request and merges it.
Merged
The pull request is merged to main after a person approves it. Nothing merges without that approval.
Definition of done
Breakdown
The feature breakdown
Business first, then the engineering view. Tab through it.
Overview
The gap it closes
Updating hundreds of records by hand is slow, and letting anyone change records in bulk is risky. Bulk import updates existing records only; it does not create them. A staff member can update only their own records, and changing the owner or reviewer stays an admin decision enforced on the server.
Who uses it
Staff update the records they own in bulk; an administrator can update across the tenant. Creating a record is done through onboarding, not through bulk import.
User story
As a staff member, I want to update my own records in bulk from one sheet, matched on a stable code, without being able to change anyone else's records or their owner and reviewer.
Examples
Example 1
A staff member uploads a sheet of updates to records they own, each matched on its stable code. The rows are staged; on an explicit Apply, every valid row is updated and a bad row is rejected one by one with a reason while the rest go through.
Example 2
A staff member uploads a sheet that smuggles in an assignment column. The server refuses the assignment change, so the records keep their existing owner and reviewer. The attempt is not just hidden in the screen.
Dependencies & risks
Depends on
The existing records and the staff list, a stable code to match each row, and a shared check for which workflow stages are locked.
Risk & mitigation
A bad or smuggled row could corrupt data. Reduced by staging first and changing nothing until an explicit Apply, committing each row on its own, rejecting bad rows with a reason, and enforcing the admin-only assignment rule on the server, not just in the screen. A rejected row is reported with its reason and can be fixed and re-applied.
How it works
Simplified diagrams of the parts involved, the steps of one request, and where it runs.
Architecture
- 1update sheet
- 2stage, then check each row
- 3apply valid row updates on Apply
- 4download the pre-filled template
Sequence
- 1upload a sheet
- 2check each row, including the locked-stage and assignment rules
- 3commit valid rows one by one
- 4a result of updated and rejected rows after Apply
Infrastructure
- 1upload a sheet
- 2read the sheet
- 3update records, write run history
Acceptance criteria
Each scenario is written as a Gherkin spec. The Engineering tab shows the test that covers each one, or notes where it was verified by review.
@customers @importFeature: Bulk import from a spreadsheetAs a staff memberI want to update many records from one sheetSo that I can change hundreds of records at once without re-keyingBulk import updates existing records only; it does not create them. Rows are matched on a stable system-generated code, a staff member can update only the records they own, and changing the owner or reviewer is refused on the server. Uploads are staged first and nothing changes until an explicit Apply.Background:Given a spreadsheet is prepared with the required columns and the stable code columnRule: Bulk import updates existing records and never creates themScenario: A sheet updates records matched on a stable codeGiven a staff member uploads a sheet of updates matched on the stable code columnWhen the batch is appliedThen the matching records are updated and no records are createdScenario: A row that matches no record is rejectedGiven a sheet row carries a code that matches no recordWhen the batch is appliedThen the row is rejected and nothing is createdScenario: Two different records with the same name are not merged silentlyGiven two records share a nameWhen the batch is stagedThen the collision is reported for confirmation, never merged on the name aloneScenario: Re-running the same file does not duplicate workGiven a batch has already been applied for a fileWhen the same file is submitted again under the same idempotency keyThen the work is not duplicatedRule: A staff import only touches the records that staff member ownsScenario: A staff import updates owned records onlyGiven a staff member uploads a sheet matched on the stable codeWhen the batch is appliedThen only records that staff member owns are updatedScenario: A staff import cannot change the owner or reviewerGiven a staff member's sheet includes an assignment columnWhen the batch is appliedThen the assignment change is refused on the serverAnd the records keep their existing owner and reviewerRule: Nothing changes until an explicit Apply, and a bad row is rejected on its ownScenario: Staging changes nothing until ApplyGiven a sheet has been uploaded and stagedWhen no Apply has been requestedThen no records have changedScenario: A blank required column rejects the rowGiven a staged sheet has a row with a blank required columnWhen the batch is appliedThen that row is rejected and the other rows are appliedScenario: A record in a locked workflow stage is rejected at apply timeGiven a record is in a locked workflow stageWhen an import tries to change itThen that row is rejectedRule: A file that is not well-formed is refused before any row is appliedScenario Outline: File-level checksGiven an admin or staff member uploads a "<file>"When the import is validatedThen the result is "<result>"Examples:| file | result || sheet with a wrong or missing header | refused with a clear message || file over the 1,000-row cap | refused || wrong file type by content bytes | refused || an archive | refused || non-Latin names and leading zeros | preserved, not mangled |Scenario: A partial run reports what applied and what did notGiven a file has a mix of good and bad rowsWhen the batch is appliedThen the good rows are appliedAnd a report lists each rejected row with its reason so it can be fixed and re-applied@edge-casesScenario Outline: Row outcomes during applyGiven an import row is "<condition>"When the batch is appliedThen the row result is "<result>"Examples:| condition | result || matches a record the staff owns | updated || matches no record | rejected, nothing created || unknown owner or reviewer | rejected || owner equal to reviewer | rejected || duplicated within the same file | reported, not applied twice |
Questions
Questions raised in planning, each with the decision taken.
What matches a sheet row to a record?
An update must hit exactly the right existing record, so the match key has to be unambiguous. A name is not unique.
What happens when a row names an unknown owner or reviewer, or the same person for both?
Auto-creating a user from a sheet, or allowing a person to own and review their own work, would both break the rules the import is meant to keep.
What happens to a record that is mid-workflow when an import tries to change it?
Changing a record that is in a locked stage could undo work that is already in review.
Can a staff member change assignments through their import?
Assignment is an admin decision. A staff import that could reassign would be a way around that rule.
How we sized it
A function point measures what the software does — its inputs, outputs, lookups and the data it stores — regardless of language or how many lines it took. Each counted item is weighted by complexity: a user input counts 3–6 points, an output or report 4–7, a lookup 3–6, data the feature owns 7–10, and data read from elsewhere 5–7.
- Admin bulk import that creates and assigns recordsUser input6
- Staff bulk update of their own recordsUser input6
- Download the import templateOutput or report5
- View the history of import runsLookup4
- Bulk field update with auto-finaliseUser input4
- Total25
Not counted here
- The records themselves: counted once, in their owning feature
Components touched
Spreadsheet update import
Update-only: it matches each row to an existing record on a stable system-generated code and does not create records. A staff member can update only the records they own; an administrator can update across the tenant.
Layered validation
Whole-file rules (naming, a 1,000-row cap, file type by content bytes), then a pinned header and column schema with a strict date format, then per-row identity and ownership checks, then a workflow-lock check at apply time. Each updated record goes through the single patch writer, so every record invariant is re-checked and audited.
Staging and explicit Apply
An upload is staged first; nothing changes in the records until an explicit Apply. Each row is applied on its own, so a bad row is rejected with a reason and can be fixed and re-applied while the rest go through.
Shared locked-stage guard
One guard that rejects an update to a record in a locked workflow stage at apply time.
Document-upload apply path
A separate document-upload apply path matches an existing record and refuses an ambiguous match for confirmation; like the spreadsheet path it never creates a record.
Data touched
Customer recordswrite
Updated by the import for the records in scope; bulk import does not create records.
Staff listread
Read to resolve owner and reviewer; a row naming an unknown person is rejected.
Import-run historywrite
A staged batch and a record of each import run and its per-row outcomes.
Audit logappend
Each applied record update goes through the single patch writer and is audited.
Security decisions
- Bulk import is update-only; it does not create records, and a row is matched on a stable system-generated code, never merged on a name.
- The admin-only assignment rule is enforced on the server, so a smuggled assignment column is refused, not just hidden in the screen.
- A staff import is limited to records that staff member owns.
- A row naming an unknown owner or reviewer, or the same person for both, is rejected; a user is never auto-created.
- An update to a record in a locked workflow stage is rejected at apply time.
- Nothing changes until an explicit Apply; each applied update runs through the single patch writer and is audited.
Subtasks
- The update-only spreadsheet import, matched on a stable code.
- Layered validation: file structure, a pinned header and column schema, per-row identity and ownership, and a workflow-lock check at apply time.
- Staging with an explicit Apply; per-row application with a reason on each rejected row.
- The shared locked-stage guard.
- The document-upload apply path matches an existing record, with ambiguity refusal.
How each scenario is verified
- A staff import updates owned records by a stable codeA bulk-import integration test updates records on the staff path, matched on a stable code; a row matching no record is rejected and creates nothing, and an unmapped match is rejected while the rest proceed.
- A staff import cannot change the owner or reviewerA server-side integration test checks that an assignment column put into a staff sheet is refused by the server, not just hidden in the screen.
- Nothing changes until an explicit ApplyA staging test confirms a fully staged batch writes zero changes until Apply, and a no-match row is flagged while the rest of the batch proceeds.
- File rules and per-row outcomesA rules and integration test cover the 1,000-row cap, file-naming and archive rejection, a same-name collision reported for confirmation rather than fuzzy-matched, and idempotency replay and concurrent submits.
- A record in a locked stage is rejected at apply timeVerified by review of the apply-time lock check.
Risk tier
Core A person approves every merge. This is a Core-tier change, so it also carries the fullest set of automated tests; a regression here affects the whole product, not one page.
Activity
Shipped across four pull requests.
- Planned, then built as four pull requests
- Each merged to main after a person approved it
Start here
Want this on your backlog?
Get a free baseline report on one repository, or scope a 3-month pilot measured against what your team’s work costs today.