This document describes the exact config resolution path implemented in apps/web/src/main.ts and packages/core/src.
Config objects are merged in the following order (higher items overwrite lower items):
- Base object:
{}. - Variant config: Selected by variant manifest
configPath, unless?config=...is provided. - Runtime overrides:
selection.overrides(JATOS overrides if present, else URLoverrides).
Effective baseline behavior comes from the selected variant config JSON.
All JSON files under configs/**/*.json are automatically bundled at build time via import.meta.glob — no explicit import is needed. However, the two URL access methods have different registration requirements:
| Access method | URL | Registration required? |
|---|---|---|
| Named variant | ?task=X&variant=myconfig |
Yes — must be listed in the task adapter variants[] manifest |
| Explicit path | ?task=X&config=X/myconfig |
No — any bundled JSON works immediately |
| Bare name (task-scoped fallback) | ?task=X&config=myconfig |
No — resolves as X/myconfig first; if myconfig matches a variant id, that variant configPath is tried first |
To iterate quickly on a new config without touching code, use ?config=:
- Place
configs/<taskId>/myvariant.json. - Open either
?task=<taskId>&config=<taskId>/myvariantor?task=<taskId>&config=myvariant.
To publish a named variant (so ?variant=<id> works), add it to tasks/<taskId>/src/index.ts:
variants: [
{ id: "myvariant", label: "My Variant", configPath: "<taskId>/myvariant" },
]The framework uses a deep merge (deepMerge). Overriding one nested key does not replace sibling keys in that object.
Runtime overrides allow you to modify an experiment without changing any JSON files.
You can pass a URL-encoded JSON object as the overrides parameter.
Example: Increase the response deadline for SFT.
http://localhost:5173/?task=sft&variant=default&overrides=%7B%22timing%22%3A%7B%22response_deadline_ms%22%3A5000%7D%7D
In a JATOS study, place your overrides in the Component JSON Input.
{
"overrides": {
"mapping": {
"targetKey": "k"
}
}
}If both JATOS and URL overrides are present, JATOS overrides win.
You can enable a built-in auto-responder to run long synthetic sessions for QA and data pipeline testing.
Use the same launch URL with auto=true:
http://localhost:5173/?task=stroop&variant=default&auto=true
Accepted truthy/falsy values:
- truthy:
1,true,yes,on - falsy:
0,false,no,off
Global defaults belong in core config (configs/core/default.json), and task configs may override with an autoresponder object.
{
"autoresponder": {
"enabled": false,
"jsPsychSimulationMode": "visual",
"continueDelayMs": { "minMs": 800, "maxMs": 2600 },
"responseRtMs": { "meanMs": 720, "sdMs": 210, "minMs": 180, "maxMs": 3200 },
"timeoutRate": 0.08,
"errorRate": 0.12,
"interActionDelayMs": { "minMs": 450, "maxMs": 1200 },
"holdDurationMs": { "minMs": 220, "maxMs": 860 },
"maxTrialDurationMs": 90000
}
}Resolution order:
coreConfig.autorespondertaskConfig.autoresponder- URL
auto=...(final override for enabled/disabled) - URL
auto_mode=visual|data-only(final override for jsPsych simulation mode)
In JATOS deployments, if the browser URL no longer contains query params after Publix redirects, core also reads launch params from jatos.urlQueryParameters (via jatos.onLoad readiness path). This preserves toggles like auto=true and participant IDs such as SONA_ID.
Behavior:
- jsPsych tasks (
sft,nback,stroop) run in jsPsych simulation mode (visualby default). - Continue screens auto-advance with sampled delays.
- Native tasks (
bricks,tracking,change_detection) auto-start and apply task-specific synthetic timing guards. - When
task.modules.drt.enabledis true:auto_mode=visual: DRT uses live runtime with synthetic key responses.auto_mode=data-only: DRT uses a virtual-time simulation path (same core DRT engine/output shape) so DRT data is still produced in fast data-only runs.
The shell background outside task stimulus frames is configurable.
Keys:
coreConfig.ui.pageBackground(global default)taskConfig.ui.pageBackground(per-task/per-variant override)
Precedence:
taskConfig.ui.pageBackgroundcoreConfig.ui.pageBackground- CSS default in
apps/web/src/styles.css
Examples:
Core default (configs/core/default.json):
{
"ui": {
"pageBackground": "#f8fafc"
}
}Task override (configs/pm/annikaHons.json):
{
"ui": {
"pageBackground": "#ffffff"
}
}Core instruction/progression buttons (waitForContinue, instruction actions, block start/end, task end) can be styled globally from config.
Keys:
taskConfig.ui.buttonStyle(ortaskConfig.ui.continueButtonStyle)taskConfig.ui.autoFocusContinueButton(defaulttrue)
Example:
{
"ui": {
"continueButtonStyle": {
"fontSize": "22px",
"padding": "14px 26px",
"borderRadius": "999px",
"outline": "none",
"boxShadow": "none"
},
"autoFocusContinueButton": false
}
}Supported style fields:
paddingfontSizefontWeightborderborderRadiuscolorbackgroundminWidthminHeightoutlineboxShadow
Survey submit buttons support analogous per-survey controls via survey definition:
submitButtonStyleautoFocusSubmitButton
The framework supports an opt-in local EEG bridge for marker forwarding.
Keys:
coreConfig.eegtaskConfig.eeg(overrides core values for the selected task run)
Example:
{
"eeg": {
"enabled": true,
"bridgeUrl": "http://127.0.0.1:8787",
"requireBridge": true,
"eventTypes": ["task_start", "task_end", "trial_start", "trial_end"],
"includeEventPayload": false
}
}Behavior:
- When
enabled=false, EEG bridge integration is inactive. - When
requireBridge=true, launch is blocked ifGET {bridgeUrl}/healthfails. - Session events listed in
eventTypesare forwarded toPOST {bridgeUrl}/event. - This is marker/event forwarding only; continuous EEG signal recording is expected to be handled by external tooling (for example LabRecorder).
The framework supports dynamic redirect URLs upon completion. These are configured in the completion.redirect section of the core config.
{participantId},{studyId},{sessionId}: Standard IDs.{PROLIFIC_PID},{STUDY_ID},{SESSION_ID}: Prolific-specific IDs.{survey_code}: The completion code found in the selection context.{taskId},{variantId}: The identifiers for the current task.
Example Config:
{
"completion": {
"redirect": {
"enabled": true,
"completeUrlTemplate": "https://app.prolific.com/submissions/complete?cc={survey_code}&pid={PROLIFIC_PID}"
}
}
}For tasks that use the shared instruction-slot parser (pm, nback, tracking, bricks, sft, stroop, change_detection, flanker), instructions supports:
pages(preferred intro pages): string, object, or array- aliases:
introPages,intro,screens
- aliases:
preBlockPages: string, object, or array (shown before every block)postBlockPages: string, object, or array (shown after every block)endPages: string, object, or array (shown before final completion screen)
Instruction page object shape:
text: plain text (escaped)html: raw HTML fragmenttitle: optional heading for that pageactions: optional button array for that page- each action:
{ "id"?: string, "label": string, "action"?: "continue" | "exit" } "exit"halts the task flow immediately and does not run completion finalization/redirect.
- each action:
Resolution behavior:
- Slot aliases are checked in priority order, and the first key that is explicitly present is used.
""(or arrays like[""]) intentionally clear that slot and prevent fallback to inherited/default pages.- Blank array entries are ignored.
Example:
{
"instructions": {
"pages": [
"Welcome.",
"This session includes N-back and PM responses.",
"Press continue when ready."
],
"preBlockPages": "Stay focused and keep your fingers on response keys.",
"postBlockPages": "Take a brief pause before continuing.",
"endPages": [
"You have completed all blocks.",
"Please continue to the final completion screen."
]
}
}For tasks that use the core orchestrator (including NBack), you can insert additional instruction pages at specific lifecycle points using:
instructions.insertions: array of insertion specs
Insertion spec fields:
at: insertion point (required)pages: string/object or array (required)id: optional label for readabilitywhen: optional block filterblockIndex: number[]blockLabel: string[]blockType: string[]isPractice: boolean
Supported at values:
task_intro_beforetask_intro_afterblock_start_before_introblock_start_after_introblock_start_after_preblock_end_before_postblock_end_after_posttask_end_beforetask_end_after
Notes:
- Multiple insertion specs at the same
atpoint are supported and run in array order. whenfilters apply to block-level insertion points.- Insertion pages are resolved through the task variable resolver, including block-local context where available.
Example:
{
"instructions": {
"pages": ["Welcome."],
"preBlockPages": "Get ready.",
"insertions": [
{ "at": "task_intro_before", "pages": ["Consent reminder."] },
{
"at": "task_intro_before",
"pages": [
{
"title": "Consent",
"html": "<iframe src=\"/assets/pm-words/consent.html\" style=\"width:min(980px,96vw);height:70vh;border:1px solid #ccc;border-radius:8px;\"></iframe>",
"actions": [
{ "label": "I Consent", "action": "continue" },
{ "label": "Disagree (exit study)", "action": "exit" }
]
}
]
},
{
"at": "block_start_after_intro",
"pages": ["Remember PM response for this block."],
"when": { "blockType": ["pm"], "isPractice": false }
},
{ "at": "task_end_before", "pages": ["Almost done."] }
]
}
}For tasks that run through the core orchestrator (including NBack), blocks can define:
repeatUntil: optional object on a block
Fields:
enabled(defaulttruewhen object is present)maxAttempts(integer, default1)minAccuracy(0..1) orminAccuracyPct(0..100)minCorrect(optional integer)minTotal(optional integer)maxMeanMetric,minMeanMetric(optional thresholds on the mean absolute value ofmetrics.metricField)where(optional trial filter object, same shape as block-summary filtering)metrics.correctField(field used for correct/incorrect scoring;true/1count as correct)metrics.metricField(optional field used for mean-metric thresholds; array values are expanded)
Example:
{
"plan": {
"blocks": [
{
"label": "Practice",
"trials": 20,
"repeatUntil": {
"maxAttempts": 3,
"minAccuracy": 0.8,
"where": { "trialType": ["N"] },
"metrics": { "correctField": "responseCorrect" }
}
}
]
}
}Notes:
- Evaluation is attempt-local and computed from that attempt's trial results.
- Retries stop as soon as thresholds are met or
maxAttemptsis reached. - Default post-block pages (
afterBlockScreens/ task-level post-block pages) are shown on the final attempt only. - Use
repeatAfterBlockScreens(aliasrepeatPostBlockScreens) on a block for retry-attempt messaging.
The framework supports dynamic variable resolution and sampling in task configurations via the core VariableResolver.
When a task is launched, the LifecycleManager automatically resolves variable tokens in the merged configuration before passing it to the task adapter's initialize method.
Important Scope Note:
- Only
participantscoped variables are resolved at this high level. blockandtrialscoped variables are left as tokens (e.g.,"$var.myVar") so that task adapters can resolve them dynamically during the experiment lifecycle.
Variables are defined in the variables section of the task configuration.
{
"variables": {
"betweenGroup": {
"scope": "participant",
"sampler": {
"type": "list",
"values": ["A", "B"]
}
},
"difficulty": {
"scope": "block",
"sampler": {
"type": "list",
"values": [1, 2, 3]
}
}
}
}$var.name: Direct variable reference.$sample.name[:count]: Samples from a variable (uses the variable's sampler).$namespace.path: References values from a specific namespace (e.g.,$local.itemIdor$between.condition).
In addition to full-token fields, any string value resolved through the core resolver can interpolate variable expressions with ${...}.
${var.name}: interpolate a variable value.${namespace.path}: interpolate values from a namespace.
Examples:
{
"variables": {
"pmCategory": "animals",
"between": {
"controlSuffix": "controls"
}
},
"plan": {
"blocks": [
{
"nbackSourceCategories": ["${var.pmCategory}_${between.controlSuffix}"]
}
]
}
}Notes:
- Existing full-token behavior is unchanged (
"$var.name"still resolves as before). - Interpolation is string-oriented; unresolved expressions are left unchanged.
The framework supports several namespaces:
var: The default namespace for variables defined in the config.local: Local values provided by the task adapter during dynamic resolution (e.g., trial-level data).- Custom namespaces: Can be registered by task adapters.
If your config changes aren't taking effect, check the following:
- Isolation Check: The framework validates
taskConfigisolation. Root-level keys belonging to other tasks (e.g., puttingmappingin an SFT config) are rejected byvalidateTaskConfigIsolation. - Schema Errors: Check the browser console. Task parsers (for example
parseSftConfig) throw descriptive errors when required sections are missing or malformed. - Variant source:
?config=...replaces variant manifest mapping for that launch. - Runtime source precedence: JATOS input overrides URL
overrides.
Instruction orchestration note:
- Active tasks now hydrate normalized instruction surfaces into
taskConfig.instructionsvia core helper (applyResolvedTaskInstructionSurfaces) and rely onTaskOrchestratorto consume them centrally (introPages,preBlockPages,postBlockPages,endPages,blockIntroTemplate,showBlockLabel,preBlockBeforeBlockIntro). sftstaircase runs through the coreTaskOrchestratorstaircase slot (taskConfig.staircase.enabled === true).
Module orchestration note:
TaskOrchestratornow resolves module config centrally from task/block/trial layers (task.modules+block.modules+trial.modules) with deep merge, and starts/stops by scope.- Tasks can provide module display targeting through
resolveModuleContextinstead of manual module lifecycle wrappers.
The web shell supports a planning/export mode for parity and audit workflows.
Use URL flag:
exportStimuli=true(orexport_stimuli=true)
Supported tasks:
sftnbackbricksstrooptrackingchange_detectionflanker
Behavior:
- Task runtime builds planned blocks/trials but skips trial execution.
- A CSV is downloaded with planned stimuli and response coding fields (including
trial_codevalues likepm,lure_<n>,target,non_target).
Current planning capabilities are task-specific on top of shared core scheduling primitives:
- Shared core primitive:
buildScheduledItemssupportsweighted,sequence,quota_shuffle,block_quota_shuffle.- shared pool runtime supports seeded source loading and draw modes (
ordered,with_replacement,without_replacement, plus categoryround_robin).
- SFT:
- block-level manipulation assignment via
design.blocks[].manipulationormanipulationPool - within-block trial-type composition via
design.manipulations[].trial_plan.variants[]+trial_plan.schedule
- block-level manipulation assignment via
- Stroop:
- balanced condition construction via quotas + adjacency constraints
- replicated block template (
plan.blockCount+plan.blockTemplate) - no manipulation-plan layer in current Stroop schema
If your goal is “define a few trial types, then combine/schedule them across blocks,” SFT exposes this directly; Stroop currently does not expose an equivalent manipulation layer.
For stimulus identity pools (e.g. NBack/PM category item draws), use task stimulusPools draw config where available:
nbackDrawpmItemDrawpmCategoryDraw
Core local-save behavior is controlled by data.localSaveFormat:
{
"data": {
"localSave": true,
"filePrefix": "experiments",
"localSaveFormat": "csv"
}
}Supported values:
csv(default): local CSV downloadjson: local JSON downloadboth: local CSV + JSON download
JATOS submission is unaffected by this setting. When JATOS is available, core emits incremental JSON-lines data through its sink path and still preserves local save behavior for testing.
Core provides a reusable module for injecting trials into an existing task plan:
- config path:
task.modules.injector - module id:
injector
Minimal shape:
{
"task": {
"modules": {
"injector": {
"enabled": true,
"injections": [
{
"id": "example",
"schedule": { "count": 3, "minSeparation": 6, "maxSeparation": 10 },
"eligibleTrialTypes": ["F"],
"source": { "type": "category_in", "categories": ["animals"] },
"sourceDraw": {
"mode": "without_replacement",
"scope": "block",
"shuffle": true
},
"set": {
"trialType": "PM",
"itemCategory": "PM",
"correctResponse": "space",
"responseCategory": "pm"
}
}
]
}
}
}
}Source modes:
category_in: draws from loaded stimulus pools by category name.literal: draws fromsource.itemsinline list.sourceDraw: controls draw behavior for injected items.mode:without_replacement(default),with_replacement,orderedscope:block(default),participantshuffle: defaults totruewithout_replacementrecycles automatically once exhausted.
Setter fields:
set.trialType(optional)set.itemCategory(optional)set.correctResponse(optional)set.responseCategory(optional semantic label used for module response semantics)
For path fields that are resolved through core stimulus/config helpers (for example basePath + path), you can use:
{runtime.assetsBase}{runtime.configsBase}
Example:
{
"stimuliCsv": {
"basePath": "{runtime.assetsBase}/pm-words",
"categories": {
"practice": "practice.csv"
}
}
}