A job post is just
the beginning.
Turn an advertisement into an application. Give reviewers the right candidate materials. Keep the evidence behind each hiring decision.
A complete local example. We will hire a fictional Backend Engineer, receive Taylor’s application, and request feedback from Alex and Sam. The walkthrough uses the demo provider: it creates local records, sends no email, and makes no provider requests. Its demo.invalid links are intentionally not live forms.
Chapter 01The hiring loop
Spence organizes the employer’s side of hiring: openings, application versions, reviewers, stages, and decisions. EDSL humanize supplies the hosted forms applicants and reviewers fill out. McCall can help improve the job post before it enters this workflow.
- 1. AdvertiseAttach a job post to an opening.
- 2. CollectPublish a versioned application.
- 3. ReviewAsk selected people for feedback.
- 4. DecideRecord a stage and a reason.
One opening can have several advertisements. An application belongs to a particular publication of one advertisement and form. Each reviewer assesses a saved application version, so an edited submission cannot silently change the evidence behind earlier feedback.
Who sees what?
This illustration shows the default boundaries; the panels contain fictional material, not a running hosted form.
The job and the application
Backend Engineer — Build Python APIs for research tools.
Taylor supplies contact details, relevant experience, and motivation. Humanize hosts the form when using the Coop provider.
A selected candidate packet
Alex sees the public job post and Taylor’s selected experience and motivation answers, then completes the rubric.
Default packets exclude contact information, internal notes, attachments, and other reviewers’ opinions.
The application and its history
The operator can inspect contact details, stage changes, internal notes, delivery status, and attributed feedback.
This alpha uses a private filesystem workspace for a trusted team. It has no employer web dashboard or separate permissions for people sharing that workspace.
Chapter 02Install and try the demo
Install Python 3.11 or newer, Git, and uv. Clone Spence and activate its environment. Run the tutorial commands in the same Bash or Zsh terminal so the saved IDs remain available.
git clone https://github.com/expectedparrot/spence.git
cd spence
uv venv .venv
uv pip install --python .venv/bin/python -e '.[dev]'
source .venv/bin/activate
spence --version
The package installs EDSL from GitHub main. Publication definitions record the installed EDSL version and commit when available.
The quick route
From the repository directory, this script creates the entire example and prints the private report path. Choose a destination that does not already exist.
python examples/demo.py /tmp/spence-quickstart
Open the saved tutorial report → This report comes from the fictional walkthrough below.
The step-by-step route
Continue below from the repository directory. The commands create a separate hiring-demo workspace beside the repository. If you already have that directory, choose another name in the next two commands.
Chapter 03Create an opening
Create the private workspace, a role brief, and the exact post applicants will read. The fixture files contain fictional data and stay inside the private directory.
cd ..
spence init hiring-demo --provider demo --owner tutorial-operator
cd hiring-demo
mkdir .spence-private/fixtures
cat > .spence-private/fixtures/role.json <<'JSON'
{
"id": "engineer",
"title": "Backend Engineer",
"criteria": ["Python API development", "Reliable services", "Clear communication"]
}
JSON
cat > .spence-private/fixtures/post.md <<'POST'
# Backend Engineer
Help our fictional research team build useful tools.
You will develop Python APIs, improve service reliability,
and explain technical tradeoffs to researchers.
Tell us about a service you built and why this work interests you.
POST
spence role import .spence-private/fixtures/role.json
spence post import .spence-private/fixtures/post.md --id advert
spence opening create --id backend --role engineer
spence opening add-post backend --post advert
The opening is now a draft. A role captures what the team needs; a post is an advertisement for that opening. For an existing McCall export, the alternative import is spence post import post.json --mccall-export, which checks its content hash.
Chapter 04Design the application
Design an EDSL Survey with the questions you want candidates to answer. The humanize schema sets requiredness, and the mapping tells Spence which answers are contact details and which may enter a review packet.
Prefer a preset?
spence application-form init --id engineering provides these four standard questions. Use either the preset or the custom import below, since an ID can only be created once.
python - <<'PYTHON'
import json
from pathlib import Path
from edsl import Survey, QuestionFreeText
folder = Path(".spence-private/fixtures")
survey = Survey([
QuestionFreeText(
question_name="candidate_name",
question_text="{{ job_post }}\n\nWhat is your name?",
),
QuestionFreeText(question_name="candidate_email", question_text="What is your email?"),
QuestionFreeText(question_name="experience", question_text="Describe a Python service you built."),
QuestionFreeText(question_name="motivation", question_text="Why does this role interest you?"),
])
survey.git.save(folder / "application.survey.ep", message="Tutorial application")
schema = {"questions": {name: {"optional": False} for name in survey.question_names}}
mapping = {
"candidate_name": "contact.name",
"candidate_email": "contact.email",
"experience": "profile.experience",
"motivation": "profile.motivation",
}
(folder / "humanize.json").write_text(json.dumps(schema, indent=2))
(folder / "fields.json").write_text(json.dumps(mapping, indent=2))
PYTHON
spence application-form import --id engineering \
--survey .spence-private/fixtures/application.survey.ep \
--schema .spence-private/fixtures/humanize.json \
--mapping .spence-private/fixtures/fields.json
spence application-form validate engineering
spence application-form build engineering --opening backend --post advert \
--output .spence-private/application.jobs.ep
The resulting Jobs artifact contains the Survey and one scenario with the public job post. It contains no models. Spence later adds a reviewer AgentList only for named human recipients.
Designs are versioned. Use --revise to update a saved form. Already prepared publications keep their original form and post snapshots. Supported questions include free text, multiple choice, checkboxes, numerical, list, and file upload, with static answer constraints and answer-based skip rules.
application-form init --id with-resume --resume adds an upload question. This alpha records provider file references, but hosted upload and access behavior still need verification. File fields cannot be included in reviewer packets yet.
Chapter 05Add an Apply link
Publish the prepared form and attach its Apply URL to an exported copy of the post. In this workspace, publication creates a local demo survey. The shell variable below reads the actual generated publication ID from Spence’s JSON output.
spence opening publish backend --post advert --form engineering \
> .spence-private/publication.json
cat .spence-private/publication.json
publication_id=$(python -c 'import json; print(json.load(open(".spence-private/publication.json"))["publication"]["id"])')
spence opening export-post backend --publication "$publication_id" \
--output post-with-apply.md
cat post-with-apply.md
The exported Markdown ends with an Apply link under https://demo.invalid/apply/…. It is a demonstration artifact. With the Coop provider, the same command returns a hosted humanize URL you can place in your job advertisement.
Chapter 06Receive an application
Taylor has built Python services and is interested in research tools. Submit those fictional answers through the demo provider, then synchronize the completed response into Spence.
cat > .spence-private/fixtures/application.json <<'JSON'
{
"candidate_name": "Taylor Example",
"candidate_email": "taylor@example.com",
"experience": "Built Python APIs and operated production services.",
"motivation": "I enjoy building useful tools for researchers."
}
JSON
spence demo submit --publication "$publication_id" \
--answers .spence-private/fixtures/application.json
spence applications sync --opening backend
spence applications list --opening backend > .spence-private/applications.json
application_id=$(python -c 'import json; print(json.load(open(".spence-private/applications.json"))["records"][0]["id"])')
spence application show "$application_id"
spence application move "$application_id" --to screening \
--reason 'Relevant experience merits a structured review.'
spence applications sync --opening backend
The first synchronization creates one application. The second reports one unchanged response. Deduplication uses provider survey and response identities; an email match only flags a possible duplicate candidate.
application show includes the submitted answers, contact details, and source information. A later response revision creates a new submission version while preserving the application’s stage. Missing or invalid answers go into quarantine.
Chapter 07Prepare two reviewers
Ask Alex and Sam to assess the same saved candidate packet. Import their fictional addresses, create a rubric, and explicitly select the application fields they will see.
cat > .spence-private/fixtures/reviewers.json <<'JSON'
[
{"id": "alex", "name": "Alex Reviewer", "email": "alex@example.com"},
{"id": "sam", "name": "Sam Reviewer", "email": "sam@example.com"}
]
JSON
spence reviewer import .spence-private/fixtures/reviewers.json
spence review-template init --id rubric
spence review prepare "$application_id" --template rubric \
--reviewer alex --reviewer sam --field experience --field motivation \
> .spence-private/batch.json
batch_id=$(python -c 'import json; print(json.load(open(".spence-private/batch.json"))["batch"]["id"])')
spence review inspect "$batch_id"
Inspect the packet and roster before publishing. Reviewers receive the public post, selected experience and motivation, and four questions: relationship to the candidate, strengths, concerns, and a recommended next step. The batch freezes the application version, template, and recipients.
To design your own feedback survey, use review-template import --id rubric --survey review.ep --schema review-ui.json instead of the preset. Review question text can refer to {{ candidate_packet }} and {{ job_post }}.
Chapter 08Collect their feedback
Publishing obtains personal respondent links. Sending creates a delivery operation. In demo mode, delivery is simulated locally and no messages leave your machine.
spence review publish "$batch_id"
spence review send "$batch_id" > .spence-private/delivery.json
operation_id=$(python -c 'import json; print(json.load(open(".spence-private/delivery.json"))["operation_id"])')
spence operation delivery-status "$operation_id"
spence reviews list --opening backend > .spence-private/assignments.json
assignment_id=$(python -c 'import json; rows=json.load(open(".spence-private/assignments.json"))["records"]; print(next(row["id"] for row in rows if row["reviewer_id"] == "alex"))')
cat > .spence-private/fixtures/review.json <<'JSON'
{
"relationship": "Reviewing the submitted materials only.",
"strengths": "Relevant Python and operations experience.",
"concerns": "Discuss testing and collaboration in an interview.",
"recommendation": "advance"
}
JSON
spence demo submit --publication "$batch_id" --assignment "$assignment_id" \
--answers .spence-private/fixtures/review.json
spence reviews sync --opening backend
spence reviews list --opening backend
Alex has now submitted; Sam is still outstanding. Delivery status and review completion are separate records.
| Reviewer | Demo delivery | Review state |
|---|---|---|
| Alex | sent | submitted |
| Sam | sent | not_started |
Feedback is attributed through the saved provider respondent-to-assignment mapping. A response lacking that identity is quarantined rather than matched by name or row order.
Personal links and reminders
spence review links "$batch_id" --output .spence-private/reviewer-links.json writes the personal links into a private file. Treat the links as access credentials. Repeated sends require --remind; once a batch contains a completed or revoked review, prepare a fresh batch for the remaining reviewers.
Chapter 09Record the next step
Record what you know and why you are moving the application forward. Here, the operator chooses to arrange an interview while acknowledging that Sam’s review is outstanding.
spence application note "$application_id" \
'Alex recommends an interview. Sam has not submitted a review yet.'
spence application move "$application_id" --to interview \
--reason 'Relevant experience and Alex’s feedback justify an interview; Sam’s review remains outstanding.'
spence application report "$application_id" \
--output .spence-private/candidate-report.html
spence backup --output .spence-private/tutorial-backup.sqlite3
Open .spence-private/candidate-report.html in a browser, or view the saved result of this walkthrough. It shows the application, attributed feedback, outstanding review, stage changes, and internal notes. It is a private employer report.
There is no automatic hiring score. Each stage change records a reason and the application version used as evidence. Reports flag reviewers who assessed an earlier version after an application changes.
You have completed the loop. The workspace now has one opening, one application in the interview stage, two review assignments, one completed review, a report, and a database backup. You can inspect it with spence opening list, spence applications list, and spence reviews list.
Chapter 10Use hosted humanize forms
The local example demonstrates Spence’s behavior. A hosted workflow additionally depends on Expected Parrot authentication and the humanize service. Before using real applicant data, complete a controlled hosted acceptance run covering forms, identity, delivery, and access.
Create a separate Coop workspace using your normal Expected Parrot authentication. Do not relabel the demo workspace or reuse its fake survey IDs.
spence init hiring-live --provider coop --account my-team --owner hiring-operator
cd hiring-live
spence capabilities
Import the real role, advertisement, form, and intended reviewers. Then use the same opening publish and opening export-post commands to obtain the hosted Apply link. Applicants complete the form in their browser; the operator runs applications sync to import responses.
Sending now has an external effect. In a Coop workspace, review send BATCH_ID sends real email to the frozen reviewer roster. Inspect the packet and intended recipients before running it. The demo submission commands do not apply to hosted forms.
Spence 0.1 has tested local workflows and client contracts. Hosted acceptance, attachment authorization, intake closure, access revocation, and provider erasure remain release gates. Read the provider contracts and outstanding tasks.
Chapter 11Operate the workspace
Recover without duplicate invitations
A publication or send timeout can occur after the provider accepts the request. Spence records pending or outcome_unknown and blocks another attempt. Inspect spence operation list, then reconcile the recorded operation with the actual provider survey or delivery ID. Use resolve-not-created only after provider inspection establishes that nothing was created.
Keep uncertain responses visible
spence quarantine list shows incomplete or unattributable submissions. Export imports need an explicit publication and --accept-revisions for changed responses. Full live reads avoid treating survey start time as a completion cursor.
Know what a status means
| Command | What is recorded | Current limitation |
|---|---|---|
opening close ID | A cutoff and a closing state | Coop intake closure is unverified; distributed links may still accept responses. |
review revoke ID | Revocation pending for an assignment | Hosted link and file access are not claimed revoked. |
application delete-request ID --reason '…' | A deletion worklist that blocks new processing | No copy is erased by this command. |
Protect the workspace
Records, raw responses, and reviewer links live in .spence-private/. The directory is owner-only and excluded from Git. Generated private files use mode 0600; the database is not encrypted. Exported reports remain sensitive wherever you save them.
The backup command copies SQLite only. Preserve the workspace configuration, private Jobs artifacts, and demo-provider state separately while Spence is stopped. Do not publish a real candidate report to your documentation site.