An EDSL Survey can be saved either as serialized JSON or as an .ep
package. JSON is useful for a quick interchange. An .ep package is intended
for work that will evolve: it keeps the survey’s current state and its Git
history together in one portable file.
This tutorial creates a package from Survey.example(), opens it up, changes
one question, and follows that change through Git.
There are two layers to keep straight:
- On disk,
example-survey.ep is a single ZIP-compatible archive. It is easy
to attach, copy, archive, or deposit in a replication package.
- Inside the archive is a real Git repository. EDSL uses that repository for
commits, history, diffs, branches, tags, and synchronization.
The .ep suffix therefore does not mean “a JSON file with another extension.”
It means “a portable EDSL object with its version history.”
Create the example Survey
Start with EDSL’s built-in example:
The example has three multiple-choice questions named q0, q1, and q2.
Save it as a package with an informative commit message:
A successful save returns package and commit information:
Your commit hash will differ. Git derives it from the commit and its contents,
so it is an identifier for that particular version.
Calling save() again without changing the survey does not create an empty
commit:
This makes repeated saves safe: history records substantive package changes,
not every call to save().
Inspect the package as an EDSL object
For ordinary use, let EDSL inspect and load the file:
The CLI writes a JSON envelope to stdout:
Load the latest committed version in Python with:
Loading binds the object to its package. A later
loaded.git.save(message="...") writes a new version back to the same .ep
file unless a different path is supplied.
Unpack the Git repository
You do not need to unpack a package to use it. Unpacking is useful when you
want to audit the representation or use lower-level Git commands:
The returned JSON includes a temporary directory in data.path and a
next_step command. Change into that directory:
The directory returned by ep unpack is a temporary inspection copy. Editing
it does not rewrite the original .ep archive. Make durable changes through
the loaded EDSL object and call save().
Because .ep is ZIP-compatible, a standard archive utility can also confirm
what is present:
The listing includes .git/HEAD, .git/objects/, .git/refs/heads/main, the
manifest, question files, and survey metadata. EDSL omits transient Git files
such as hooks when packing the archive.
Read the working-tree files
The manifest identifies the format and reconstructs question order:
The exact edsl_version reflects the installed version that wrote the
package.
Each question is stored separately. For example,
questions/000001.json contains:
The split representation is deliberate. If a survey has 500 questions and
one changes, Git does not need to treat one giant serialized survey document
as the unit of review. The diff can focus on the affected question and any
metadata that actually changed.
The files under metadata/ preserve behavior that is not contained in an
individual question:
rule_collection.json stores survey flow, including skip and stop rules.
memory_plan.json records which earlier answers are made available later.
question_groups.json records named groups.
Optional survey properties, when present, are stored in additional metadata
files such as name.json, questions_to_randomize.json, and
options_to_pin.json.
Make and commit a change
Change the wording of the first question:
The package remains one file, but it now contains two commits. View them
without unpacking:
Again, hashes and timestamps will differ.
Now ask Git what changed:
The essential part of the diff is:
Three identifiers are doing different jobs:
0 is the question’s current position in the Survey.
q0 is its EDSL question_name. Prompts, rules, and results use this name
to refer to the logical question.
000001 is its stable package file ID. It is not a position and it is not
the public question name.
The manifest and the other two question files do not change. A genuinely new
question receives the next available package ID; deleting a question removes
its file from the current tree while retaining it in Git history.
The package ID also survives a pure question_name rename:
That is why the package needs an identity separate from question_name:
renaming a question is a one-line edit inside a stable file, not a file rename
and not a new question.
Return to the unchanged main line before continuing:
What Git stores
The .git directory does not contain a second full copy of the survey for
every version. Git stores content-addressed objects:
- blobs hold file contents;
- trees describe directory snapshots;
- commits point to a tree, a parent commit, and commit metadata;
- refs such as
main and tags give memorable names to commits.
From the unpacked repository, inspect the current commit:
Representative output:
Follow the tree to a stored file:
The last two commands read different survey versions directly from Git
without changing the working tree.
Git addresses every stored object by a hash of its contents. Files that did
not change are reused by the new tree instead of being stored as new logical
versions. When repositories are packed or transferred, Git can also delta
compress similar objects. This is why Git can communicate a small survey
revision without treating the entire history as an opaque replacement file.
The .ep archive itself is repacked after a save, so copying the file with a
generic file-copy tool still copies the whole archive. The transfer
efficiency comes into play when the embedded repository is synchronized with
a Git remote through EDSL’s push and pull operations.
Load or render an older version
Save the first commit ID before making further changes:
Load that exact historical survey:
The current version remains:
The CLI can also render a historical version to HTML:
This is useful in review or replication: a commit ID identifies the exact
survey being discussed, even after the package has moved forward.
Name important versions
Commit IDs are precise but not memorable. Add a Git tag to a milestone:
List tags:
Tags are helpful for statements such as “the preregistered instrument was
pilot-v1” or “production used field-v2.” A verifier can resolve the tag to
the exact commit and inspect the difference between milestones.
Explore alternatives with branches
Branches let collaborators develop alternative instruments without
overwriting the main line:
Deleting one question changes more than one file:
questions/000002.json is deleted, not renumbered. The other question files
retain their IDs. The manifest changes because the question order no longer
includes 000002; the memory plan and flow rules change because they describe
relationships among the remaining questions. This is why metadata is split
into named files rather than buried in one survey blob: Git shows exactly
which parts of survey behavior changed.
Methods that return a changed list use “changed” in Git’s broad sense:
touched paths, including additions, modifications, and deletions.
This branch represents a genuine alternative instrument, so do not merge it
merely because it exists. Keep it parallel, field or review it independently,
and return to main:
switch() refreshes the in-memory Survey from the selected branch. EDSL
requires a clean package before switching, pulling, or checking out another
version, which protects uncommitted changes.
Merge independent work
Merging is appropriate when reviewed work belongs back on the main survey.
The useful case is not a sequential edit forced into a merge commit. It is two
authors making divergent commits from the same base.
Alice and Bob begin with the current main. Create Bob’s branch before either
person edits:
Bob changes only the options for q2:
Meanwhile, Alice changes only the wording of q0 on main:
The histories now diverge. Neither commit is an ancestor of the other, and the
edits live in different question files:
Now perform a genuine three-way merge:
No no_ff=True is needed: Alice’s and Bob’s commits are genuinely divergent,
so Git must reconcile them. changed lists the paths introduced to Alice’s
pre-merge main; her q0 edit was already there, while Bob’s q2 edit arrives
through the merge.
The method completes all three layers of the operation: it merges the embedded
repository, reloads the merged files into the in-memory Survey, and repacks
the repository into example-survey.ep. The object is ready to use
immediately:
Both authors’ changes survive:
From an unpacked inspection copy, Git can display the topology:
This is the payoff from storing questions separately. Git performs a real
three-way merge, sees that Alice and Bob changed disjoint files, and retains
both changes without human intervention. With one large survey.json, Git
would be reconciling edits inside the same 4,000-line blob.
What happens on a conflict
Now repeat the experiment with both authors editing q0. Starting from a
one-question package makes the conflict especially clear:
EDSL captures the conflicted file before aborting. Its question_text conflict
is local and legible:
This is a conflict that a reviewer can understand and resolve. It is isolated
to one small question file rather than buried among thousands of unrelated
survey lines. The marker lines are indented one space above only so Git does
not mistake this documentation example for an unresolved repository conflict.
EDSL then aborts the merge, leaving the current branch and .ep archive
unchanged and clean. A researcher can use conflict_contents to inspect the
two alternatives, revise one branch, and try again. The API never silently
chooses a survey design or leaves the portable package in a half-merged state.
Communicate only the changes
An .ep file is a self-contained handoff: its recipient gets both the current
Survey and its history. For ongoing collaboration, attach a GitHub remote and
push:
What lands on GitHub is the unpacked tree of readable JSON, not an opaque ZIP
diff. A collaborator can review a one-line question-wording change as an
ordinary pull request. Git negotiates which objects the receiver already has
and transfers the missing objects, while EDSL repacks the history into .ep
when it returns to the portable-file workflow. Zipping does not defeat the
point: the archive is the transport artifact; the embedded repository is the
collaboration artifact.
Working inside another Git repository
An .ep package contains its own Git repository. If it is placed inside a
different Git repository, the outer Git repository sees an embedded
repository. EDSL warns about this because the relationship should be
intentional.
Usually, choose one of these approaches:
- Ignore the
.ep package in the outer repository and let its own Git history
and remote manage it.
- Intentionally manage it as a Git submodule.
- Store an exported JSON snapshot in the outer repository when independent
.ep history is unnecessary.
For the first option, a loaded package can update the outer .gitignore:
Why this design is useful
For survey research, a Git-backed object provides several practical
guarantees:
- Exact references. A commit hash identifies the complete survey,
including questions, order, rules, memory, and groups.
- Readable review. Questions and metadata are formatted JSON, so a
reviewer can see wording and logic changes as ordinary diffs.
- Focused changes. Unchanged questions retain their stored objects;
revisions affect the relevant question and dependent metadata.
- Recoverable history. Old instruments remain loadable even after the
working version changes.
- Named milestones. Tags can identify preregistration, pilot, field, and
publication versions.
- Parallel development. Branches can hold experimental variants.
- Efficient synchronization. Git remotes exchange missing objects rather
than treating every revision as an unrelated dataset.
- Portable provenance. Copying one
.ep file carries the current object
and its complete embedded history.
The result works at two levels. Researchers use a normal EDSL Survey;
reviewers and collaborators see ordinary Git history and readable diffs.
Agents can understand and operate the versioning plumbing so researchers do
not have to—but the repository remains available whenever a human needs to
audit exactly what changed.
Command reference