Overview
Before running your study pipeline you need two types of inputs:
- Cohorts — study populations, comparators, and outcomes as CIRCE JSON definitions (from ATLAS or Capr) or custom SQL
- Concept Sets — phenotype definitions for diseases, exposures, covariates, etc.
Picard tracks both through manifests — SQLite databases that record every definition’s file path, MD5 hash, metadata, and provenance. Each session you load the manifest into memory; the SQLite file is the durable source of truth.
This guide focuses on loading and registering inputs for day-to-day pipeline use. For mid-cycle operations such as check, update, delete, and reset, use the Manifest Management vignette.
Pre-Pipeline Builder Scripts
Every picard study is initialized with a set of builder
scripts in dedicated folders under inputs/:
-
inputs/conceptSets/R/— Scripts for building concept set manifests -
inputs/cohorts/R/— Scripts for building cohort manifests
These scripts are pre-populated at project initialization with templates for six different builder types. You choose which builders to use by keeping the scripts you need and deleting the ones you do not.
When you run main.R, the pipeline
automatically discovers and sources all remaining builder scripts in a
mandatory dependency order. This ensures concept sets
load before cohorts:
- ✅ No manual
source()calls needed inmain.R - ✅ No
main.Redits required when deleting scripts - ✅ Each builder script is self-contained with embedded guidance comments
- ✅ Mandatory source order prevents dependency conflicts
- ⚠️ Builder scripts must go in
inputs/cohorts/R/andinputs/conceptSets/R/— NOT inanalysis/tasks/
Available Builder Script Types
Concept Sets
| Script | Purpose |
|---|---|
import_atlas_concept_set.R |
Bulk import concept sets from ATLAS via CSV + connection |
import_capr_concept_set.R |
Build concept sets programmatically using Capr cs() functions |
Cohorts
| Script | Purpose |
|---|---|
import_atlas_cohort.R |
Bulk import cohorts from ATLAS via CSV + connection |
import_capr_cohort.R |
Build cohorts programmatically using Capr library |
import_sql_cohort.R |
Load custom SQL-based cohorts |
build_dependent_cohorts.R |
Create derived cohorts (temporal, union, complement, etc.) |
Typical Workflow
- Project initializes with all 6 builder scripts pre-created
- Edit the builders you need - Each script has clear guidance comments
- Delete unused builders - Remove scripts you do not need
-
Run
main.R-sourceInputBuilderScripts()auto-discovers and runs remaining scripts in mandatory order - Manifests load - Your cohorts and concept sets are ready for the pipeline
Example: If you only use ATLAS for concept sets and Capr for cohorts:
inputs/conceptSets/R/
✓ import_atlas_concept_set.R
✗ import_capr_concept_set.R (deleted)
inputs/cohorts/R/
✗ import_atlas_cohort.R (deleted)
✓ import_capr_cohort.R
✗ import_sql_cohort.R (deleted)
✗ build_dependent_cohorts.R (deleted)
When main.R runs, only
import_atlas_concept_set.R and
import_capr_cohort.R source (in order: concept sets first,
then cohorts).
Concept Set Import
Importing Concept Sets from ATLAS
This pattern uses
inputs/conceptSets/R/import_atlas_concept_set.R.
Prerequisite setup for manifests, load files, and credentials is covered in Launching a Picard Study.
Step 1: Connect to ATLAS and import
# Credentials are automatically read from ~/.picard/secrets.yml
atlasConnection <- getAtlasConnection()
conceptSetManifest$setAtlasConnection(atlasConnection)
# Read the CSV file
conceptSetsLoad <- readr::read_csv(
here::here("inputs/conceptSets/conceptSetsLoad.csv"),
show_col_types = FALSE
)
# Import
conceptSetManifest$importAtlasConceptSets(
conceptSetsLoad = conceptSetsLoad,
atlasConnection = atlasConnection
)This downloads JSON definitions to
inputs/conceptSets/json/ and updates your manifest with
metadata.
Tip: You can also pass the dataframe directly without reading from a file, which is useful for programmatic workflows.
Step 2: Load and review
conceptSetManifest <- loadConceptSetManifest()
conceptSetManifest$tabulateManifest()Auto-sync: loadConceptSetManifest()
scans inputs/conceptSets/json/ and reconciles it against
the database — updated hashes are picked up, records whose file has
disappeared are flagged as missing, and any .json file that
isn’t already registered in the manifest is treated as an orphan and
deleted. Register new concept sets explicitly
(e.g. $addConceptSetFile(),
$addCaprConceptSet(), or
importAtlasConceptSets()) rather than dropping JSON files
directly into json/ — an unregistered file will be removed
the next time the manifest is loaded.
Other import patterns for Concept Sets
Use this pattern when you want to supplement ATLAS-imported concept sets with programmatic Capr definitions, then combine them into one manifest entry.
library(Capr)
conceptSetManifest <- loadConceptSetManifest()
# Add Capr concept set example 1
metforminCs <- cs(descendants(1503297), name = "metformin")
conceptSetManifest$addCaprConceptSet(
caprConceptSet = metforminCs,
label = "Metformin",
category = "Diabetes Treatments",
tags = list(source = "capr")
)
# Add Capr concept set example 2
empagliflozinCs <- cs(
descendants(45774751), # empagliflozin
name = "empagliflozin"
)
conceptSetManifest$addCaprConceptSet(
caprConceptSet = empagliflozinCs,
label = "Empagliflozin",
category = "Diabetes Treatments",
tags = list(source = "capr")
)
# get the ids
metforminId <- conceptSetManifest$queryConceptSetsByLabel("Metformin")$id
empagliflozinId <- conceptSetManifest$queryConceptSetsByLabel("Empagliflozin")$id
# Combine concept sets (works for IDs from ATLAS, Capr, or mixed sources)
conceptSetManifest$combineConceptSets(
conceptSetIds = c(metforminId, empagliflozinId),
combinedLabel = "Diabetes Treatments",
combinedCategory = "Treatment Group",
combinedTags = list(owner = "epi_team")
)
conceptSetManifest$tabulateManifest()Cohort Import
Builder Pattern 1: Importing Cohorts from ATLAS
This pattern uses
inputs/cohorts/R/import_atlas_cohort.R.
Prerequisite setup for manifests, load files, and credentials is covered in Launching a Picard Study.
Step 1: Connect to ATLAS and import
# Credentials are automatically read from ~/.picard/secrets.yml
atlasConnection <- getAtlasConnection()
cohortManifest$setAtlasConnection(atlasConnection)
# Read the CSV file
cohortsLoad <- readr::read_csv(
here::here("inputs/cohorts/cohortsLoad.csv"),
show_col_types = FALSE
)
# Import
cohortManifest$importAtlasCohorts(
cohortsLoad = cohortsLoad,
atlasConnection = atlasConnection
)Downloads CIRCE JSON definitions to inputs/cohorts/json/
and records each cohort in SQLite.
Tip: You can also pass the dataframe directly without reading from a file, which is useful for programmatic workflows.
Builder Pattern 2: Capr-Based Building
This pattern uses inputs/cohorts/R/import_capr_cohort.R
and requires the Capr package.
AI agent support: study repos ship with a
picard-capr-cohorts skill in .agent/skills/
that wraps Capr’s own capr-cohort-generation skill. A
coding agent (Claude Code, Copilot, Cursor, …) can generate the
validated Capr code and append it to the builder script; you review and
source the script yourself.
Capr provides a fluent interface for building cohort definitions in R:
library(Capr)
cohortManifest <- loadCohortManifest()
# ckd concept set
ckdCs <- cs(descendants(46271022), name = "Chronic Kidney Disease")
t2dCs <- cs(descendants(201826), name = "Type 2 diabetes")
# Example: Chronic Kidney Disease with no prior Type 2 Diabetes cohort
# 2 CKD codes 365d apart no prior T2D
ckdCohort <- cohort(
entry = entry(
conditionOccurrence(ckdCs,
nestedWithAll(
atLeast(
1L,
conditionOccurrence(ckdCs),
aperture = duringInterval(eventStarts(-365, -1))
)
)
),
observationWindow = continuousObservation(priorDays = 0L, postDays = 0L),
primaryCriteriaLimit = "First"
),
attrition = attrition(
"No T2D on or before index" = withAll(
exactly(
0L,
conditionOccurrence(t2dCs),
aperture = duringInterval(eventStarts(-Inf, 0))
)
),
expressionLimit = "First"
),
exit = exit(
endStrategy = observationExit()
),
era = era(eraDays = 0L)
)
cohortManifest$addCaprCohort(
caprCohort = ckdCohort,
label = "Chronic Kidney Disease",
category = "Target"
)See the Capr documentation for detailed examples.
Builder Pattern 3: Custom SQL Cohorts
This pattern uses
inputs/cohorts/R/import_sql_cohort.R.
Custom SQL cohorts let you define cohorts using hand-written SQL
queries. Place your SQL files in inputs/cohorts/sql/:
cohortManifest <- loadCohortManifest()
# Add a custom SQL cohort (the file must already exist in inputs/cohorts/sql/)
cohortManifest$addSqlCohort(
filePath = here::here("inputs/cohorts/sql/my_custom_cohort.sql"),
label = "My Custom Cohort",
category = "Custom",
tags = list(source = "sql")
)SQL files must follow SqlRender conventions with parameters prefixed
by @:
-- Cohort: Patients with Type 2 Diabetes
DELETE FROM @target_database_schema.cohort
WHERE cohort_definition_id = @target_cohort_id;
INSERT INTO @target_database_schema.cohort
(cohort_definition_id, subject_id, cohort_start_date, cohort_end_date)
SELECT
@target_cohort_id as cohort_definition_id,
person_id as subject_id,
condition_start_date as cohort_start_date,
DATEADD(day, 365, condition_start_date) as cohort_end_date
FROM @cdm_database_schema.condition_occurrence
WHERE condition_concept_id IN (201820, 443238)
AND condition_start_date >= '2015-01-01';Key SqlRender parameters: - @target_cohort_id — The
numeric ID for your cohort - @target_database_schema — The
schema where results will be written - @cdm_database_schema
— The CDM database schema location -
@vocabulary_database_schema — The vocabulary schema
location
Always use
DELETEbeforeINSERTto make your cohort idempotent (can be re-run without duplication).
Builder Pattern 4: Derived Cohorts
This pattern uses
inputs/cohorts/R/build_dependent_cohorts.R.
Derived cohorts are relationships between existing base cohorts. All base cohorts must be imported first (via ATLAS, Capr, or SQL).
Start by loading the cohort manifest.
cohortManifest <- loadCohortManifest()Assume these base cohorts already exist in your manifest:
- CohortId 1: Chronic Kidney Disease
- CohortId 2: Type 2 Diabetes
- CohortId 3: Major Bleeding Outcome
- CohortId 4: All-Cause Death
For derived builders, prefer passing manifest query rows (entries) instead of raw IDs.
ckdEntry <- cohortManifest$queryCohortsByLabel(
labels = "Chronic Kidney Disease",
matchType = "exact"
)
t2dEntry <- cohortManifest$queryCohortsByLabel(
labels = "Type 2 Diabetes",
matchType = "exact"
)
bleedEntry <- cohortManifest$queryCohortsByLabel(
labels = "Major Bleeding Outcome",
matchType = "exact"
)
deathEntry <- cohortManifest$queryCohortsByLabel(
labels = "All-Cause Death",
matchType = "exact"
)Example 1: Temporal Subset
Build a cohort of CKD patients with a T2D event in a start-date window from 365 days before to 0 days after the base cohort start date.
startWindow <- createSubsetStartWindow(
subsetCohortWindowAnchor = "cohort_start_date",
startDays = -365,
endDays = 0,
baseCohortWindowAnchor = "cohort_start_date"
)
cohortManifest$buildSubsetCohortTemporal(
label = "CKD_With_Prior_T2D",
category = "Derived Cohorts",
baseCohortEntry = ckdEntry,
filterCohortEntry = t2dEntry,
startWindow = startWindow
)Example 2: Union Cohort
Build a cohort that includes anyone in either CKD or T2D.
cohortManifest$buildUnionCohort(
label = "CKD_or_T2D",
category = "Derived Cohorts",
cohortEntries = dplyr::bind_rows(ckdEntry, t2dEntry),
gapDays = 0L
)Example 3: Complement Cohort
Build a CKD cohort that excludes patients in T2D.
cohortManifest$buildComplementCohort(
label = "CKD_Without_T2D",
category = "Derived Cohorts",
populationCohortEntry = ckdEntry,
excludeCohortEntries = t2dEntry
)Example 4: Composite Cohort
Build a cohort requiring membership in multiple component cohorts (intersection style criteria).
cohortManifest$buildCompositeCohort(
label = "CKD_and_T2D_Composite",
category = "Derived Cohorts",
criteriaCohortEntries = dplyr::bind_rows(ckdEntry, t2dEntry),
minEventCount = 2L,
eventSelection = "First"
)Example 5: Demographic Subset Cohort
Build a demographic subset of CKD patients (for example, age and sex criteria).
cohortManifest$buildDemographicCohort(
label = "CKD_Males_40_to_75",
baseCohortEntry = ckdEntry,
category = "Derived Cohorts",
minAge = 40L,
maxAge = 75L,
genderConceptIds = c(8507)
)Example 6: Stratified Cohorts
Split one base cohort into multiple strata plus an automatic Unclassified cohort.
Example 7: O-Prior-T Cohort
Filter outcome events to those with prior target exposure in a 30-day lookback window.
cohortManifest$buildOPriorT(
label = "Outcome_Prior_Target",
category = "Derived Cohorts",
outcomeCohortEntry = bleedEntry,
targetCohortEntry = t2dEntry,
mode = "prior",
priorTimeWindowDays = 30L
)Example 8: T-Prior-O Cohort
Filter target events to those with prior outcome occurrence in a 30-day lookback window.
cohortManifest$buildTPriorO(
label = "Target_Prior_Outcome",
category = "Derived Cohorts",
targetCohortEntry = t2dEntry,
outcomeCohortEntry = bleedEntry,
mode = "prior",
priorTimeWindowDays = 30L
)Example 9: Censor Cohort
Create a censored version of a target cohort using a censoring event cohort.
cohortManifest$buildCensorCohort(
label = "T2D_Censored_At_Death",
category = "Derived Cohorts",
targetCohortEntry = t2dEntry,
censorCohortEntry = deathEntry
)Legacy ID inputs remain supported for backward compatibility, but entry-based inputs are recommended for new code.
Build methods shown above: - buildSubsetCohortTemporal()
- buildUnionCohort() - buildComplementCohort()
- buildCompositeCohort() -
buildDemographicCohort() -
buildStratifiedCohorts() - buildOPriorT() -
buildTPriorO() - buildCensorCohort()
See Manifest Management for comprehensive examples of all derived cohort types.
Build Pattern 5: Custom Dependent SQL Cohorts
Use this pattern when a custom SQL cohort depends on one or more
previously defined cohorts (for example inclusion/exclusion cohorts).
This registers the cohort as a dependency-aware derived type
(custom_derived) so execution order, stale detection, and
dependency hashing are handled automatically.
Like the built-in derived cohort builders (subset, union, etc.), the
values you supply are rendered into the SQL immediately and the rendered
file is written to inputs/cohorts/derived/<label>.sql
— that generated file, not your source file, is what’s registered in the
manifest. Only the connection/schema placeholders
(@target_cohort_id and friends) are left unrendered, for
generateCohorts() to fill in at execution time.
cohortManifest <- loadCohortManifest()
# Preferred: entries from manifest query results
inclusionEntry <- cohortManifest$queryCohortsByLabel("Inclusion cohort")
exclusionEntry <- cohortManifest$queryCohortsByLabel("Exclusion cohort")
cohortManifest$addDependentCustomCohort(
filePath = here::here("inputs/cohorts/sql/my_custom_dependent.sql"),
label = "Eligible_With_Exclusions",
category = "Derived Cohorts",
dependentCohortIdList = list(
inc_cohort_id = inclusionEntry,
exc_cohort_id = exclusionEntry
),
# Optional: any other values (not cohort IDs) to render into the SQL too
sqlParameters = list(
min_days = 30L
),
tags = list(owner = "epi_team", source = "custom_sql")
)How it works:
-
dependentCohortIdListis a named mapping of SqlRender parameter name to a dependent cohort. Parameter names are flexible (for exampleinc_cohort_id,exc_cohort_id,baseline_cohort_id).- Preferred: a manifest entry (a data.frame/tibble with an
idcolumn, as returned byqueryCohortsByLabel()and similar query methods) — a single row bakes in one ID, a multi-row table bakes in a comma-separated vector of IDs, forIN (@param)clauses. Same pattern asbaseCohortEntry/cohortEntrieson the built-in derived builders. - Backward compatible: a raw integer cohort ID (or integer vector).
- All referenced cohort IDs must already exist in the manifest, and
they become this cohort’s
depends_onparents for staleness tracking.
- Preferred: a manifest entry (a data.frame/tibble with an
-
sqlParametersis an optional named mapping of SqlRender parameter name to any other value (thresholds, dates, strings, etc.) you want baked into the SQL. These are not treated as cohort dependencies. - Both are rendered into the SQL at registration time — not at runtime.
- When
dependentCohortIdListentries are manifest entries with alabelcolumn, those labels (purely for QC) are written into a generated comment header at the top of the derived SQL file — e.g.inc_cohort_id: id 1001, label Inclusion cohort— so anyone reading the generated file can see at a glance which cohorts were baked in, without cross-referencing the manifest.
Your SQL file should reference the mapped placeholders:
DELETE FROM @target_database_schema.@target_cohort_table
WHERE cohort_definition_id = @target_cohort_id;
INSERT INTO @target_database_schema.@target_cohort_table
(cohort_definition_id, subject_id, cohort_start_date, cohort_end_date)
SELECT
@target_cohort_id,
i.subject_id,
i.cohort_start_date,
i.cohort_end_date
FROM @target_database_schema.@target_cohort_table i
LEFT JOIN @target_database_schema.@target_cohort_table e
ON i.subject_id = e.subject_id
AND e.cohort_definition_id = @exc_cohort_id
WHERE i.cohort_definition_id = @inc_cohort_id
AND e.subject_id IS NULL;Required contract for dependent custom SQL:
- Must
DELETEfrom@target_database_schema.@target_cohort_tableusing@target_cohort_id. - Must
INSERTinto@target_database_schema.@target_cohort_tablewith columns(cohort_definition_id, subject_id, cohort_start_date, cohort_end_date).
This ensures custom dependent SQL cohorts behave consistently with other derived cohorts in manifest review and pipeline execution.
For dependency internals (dependency graph ordering,
dependency_rule storage, and stale/hash behavior), see Manifest Management.
Templating Strategies
A common workflow for custom cohorts is to template the sql file and build multiple variations of a custom cohort with the template. When doing this the user needs to make two folders:
-
inputs/cohorts/R/src- folder holding R functions to put templates together -
inputs/cohorts/R/src/sql- folder holding the sql templates to render in the R function.
Here is an example. In a study I needed to create a derived custom
cohort called a censored complement. First I provide the custom sql I
need and place it in the inputs/cohorts/R/src/sql
folder.
/*
Make a censored complement. This means find all persons who do not have the exclusion ever or
track them until the day before the exculsion criteria occurs.
For example I want exclude anyone with CKD prior to T2D. But include T2D persons who had CKD after index date
up till the day before CKD index.
*/
DELETE FROM @target_database_schema.@target_cohort_table WHERE cohort_definition_id = @target_cohort_id;
INSERT INTO @target_database_schema.@target_cohort_table (cohort_definition_id, subject_id, cohort_start_date, cohort_end_date)
WITH T1 AS (
/*
Find the persons where the exclusion is after the inclusion start date
Adjust the end date to the day before the combination of three occurs
*/
SELECT
a.subject_id,
a.inc_cohort_start_date AS cohort_start_date,
a.exc_cohort_start_date - 1 AS cohort_end_date
FROM (
SELECT
inc.subject_id,
inc.cohort_start_date AS inc_cohort_start_date,
inc.cohort_end_date AS inc_cohort_end_date,
exc.cohort_start_date AS exc_cohort_start_date,
exc.cohort_end_date AS exc_cohort_end_date,
ROW_NUMBER() OVER (PARTITION BY inc.subject_id ORDER BY inc.cohort_start_date) AS rn
FROM @target_database_schema.@target_cohort_table inc
INNER JOIN @target_database_schema.@target_cohort_table exc
ON inc.subject_id = exc.subject_id AND exc.cohort_definition_id = @inc_cohort_id
WHERE inc.cohort_definition_id = @exc_cohort_id
AND exc.cohort_start_date >= inc.cohort_start_date
) a
UNION ALL
/*
Find the persons where the exclusion never occurs to those with the inclusion
*/
SELECT
pop.subject_id,
pop.cohort_start_date,
pop.cohort_end_date
FROM @target_database_schema.@target_cohort_table pop
LEFT JOIN (
SELECT
subject_id
FROM @target_database_schema.@target_cohort_table
WHERE cohort_definition_id IN (@exc_cohort_id)
GROUP BY subject_id
HAVING COUNT(DISTINCT cohort_definition_id) >= 1
) excluded
ON pop.subject_id = excluded.subject_id
WHERE pop.cohort_definition_id = @inc_cohort_id
AND excluded.subject_id IS NULL
)
SELECT
@target_cohort_id AS cohort_definition_id,
subject_id,
cohort_start_date,
cohort_end_date
FROM T1;Notice that in this template I am using cohorts I have already created but building a custom derivation to analyze. I want to do this for several inclusion/exclusion pairs in my manifest.
Because addDependentCustomCohort() renders
dependentCohortIdList/sqlParameters into the
SQL and writes the result to
inputs/cohorts/derived/<label>.sql itself, the
template file in inputs/cohorts/R/src/sql can be reused
as-is, unmodified, for every pair — there’s no need to hand-copy or
pre-render it first. I apply this sql template in an R function to build
each custom derived cohort from the manifest:
buildCensoredComplement <- function(
cohortManifest,
inclusionEntry,
exclusionEntry,
inputsDir = here::here("inputs/cohorts/R")
) {
sqlTemplatePath <- fs::path(inputsDir, "src/sql/censored_complement.sql")
cohortLabel <- glue::glue("{inclusionEntry$label[1]} - {exclusionEntry$label[1]} exclusion")
check_if_there <- is.null(cohortManifest$queryCohortsByLabel(cohortLabel))
if (!check_if_there) {
cli::cli_alert_warning("Censored Complement {.val {cohortLabel}} already exists. Skipped!")
} else {
cli::cli_alert_info("Create Censored Complement for {.val {cohortLabel}}")
cohortManifest$addDependentCustomCohort(
filePath = sqlTemplatePath,
label = cohortLabel,
category = "Target Sub Pop",
dependentCohortIdList = list(
inc_cohort_id = inclusionEntry,
exc_cohort_id = exclusionEntry
)
)
}
invisible(cohortLabel)
}addDependentCustomCohort() reads
sqlTemplatePath, renders @inc_cohort_id/
@exc_cohort_id with the literal cohort IDs, and writes the
result to its own generated file under
inputs/cohorts/derived/ — so the same template is safely
reused across calls, since each call produces its own uniquely named
output (named after label), never overwriting the template
or another cohort’s file.
This templating strategy can also be applied to
$addSqlCohort, which does not render or bake
parameters into the file — keep the template in
inputs/cohorts/R/src/sql and pre-render it yourself (for
example with SqlRender::render()) before registering, since
addSqlCohort() registers whatever file it’s given
as-is.
Subsequent Sessions
After the first-time import, subsequent sessions only need the manifest load calls:
conceptSetManifest <- loadConceptSetManifest()
cohortManifest <- loadCohortManifest()Both functions read from SQLite and rebuild the in-memory R6 objects. No network connection or CSV file is required.
These calls are included in the default builder scripts and will run
automatically when main.R executes
sourceInputBuilderScripts().
What’s Next
| Task | Where to go |
|---|---|
| Study setup and editable inputs | Launching a Picard Study |
| Manifest checks, updates, delete, and reset | Manifest Management |
| Running the analysis pipeline | Running the Pipeline |
| Pipeline development and testing | Developing the Pipeline |
| Creating a new study | Launching a Picard Study |