Spence artwork featuring a peacock with its tail fanned inside brackets
Expected Parrot · Worked package tutorial

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.

Spence 0.1 alphaPython 3.11+EDSL GitHub main

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. 1. AdvertiseAttach a job post to an opening.
  2. 2. CollectPublish a versioned application.
  3. 3. ReviewAsk selected people for feedback.
  4. 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.

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.

Terminal
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.

Terminal
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.

Terminal
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.

Terminal
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 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.

Terminal
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.

Terminal
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.

Terminal
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.

Expected state after this step
ReviewerDemo deliveryReview state
Alexsentsubmitted
Samsentnot_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.

Terminal
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.

Hosted workspace setup · run outside the demo directory
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

Hosted controls in the current alpha
CommandWhat is recordedCurrent limitation
opening close IDA cutoff and a closing stateCoop intake closure is unverified; distributed links may still accept responses.
review revoke IDRevocation pending for an assignmentHosted link and file access are not claimed revoked.
application delete-request ID --reason '…'A deletion worklist that blocks new processingNo 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.