Skip to contents

The following vignette will demonstrate the basic functionality (i.e., awesome might) of c2z.

Collections

Your computer desktop reveals your D&D alignment. Similarly, the way you structure your Zotero library through collections reveals your personality. (Chaotic neutral is not using collections, obviously.)

c2z lets you create nested collections on a whim, as seen below.

Creating collections

In this example we define a vector of nine elements based on “the quick brown fox jumps over the lazy dog”. We use the Zotero function, a wrapper that connects with the Zotero API and combines all major functions in c2z, to create nine nested collections based on the string (i.e., “dog” nested in “lazy” nested in “the” et cetera). We set library = TRUE, thereby querying the Zotero library, and create = TRUE to create any collections that does not exist (i.e., all nine collections).

This is a recursive action, so let’s keep the noise at a minimum and set silent = TRUE.

# Let's create a vector of nested collections
collection.names <- c(
  "The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"
  )

# Create the collections
create.collections <- Zotero(
  collection.names = collection.names,
  library = TRUE,
  create = TRUE,
  silent = TRUE
)

Using collections

Now that we have created some collections, we can access the collections in different ways. By default, the function will find the last element in the vector (i.e., “dog”).

# Dog
create.collections$collections  |>
  dplyr::select(c(key, name, parentCollection))
#> # A tibble: 1 × 3
#>   key      name  parentCollection
#>   <chr>    <chr> <chr>           
#> 1 Y6CZ8PQ9 dog   6DIEZZEJ

However, we often want access all, or some, nested collections. We can achieve this using two different approaches. 1) using recursive = TRUE to access the given element and then recursively find the nested collections, or 2) using ancestor = TRUE and trace the lineage of the specified collection.

You may use collection.name (you don’t have several top-level collections with the same name, do you? If so, the function will select the newest collection with the specified name), or collection.key if you keep track of such things. c2z will by default look for collections names regardless of letter case, and if this is important set case.insensitive = FALSE.

All collections

# Find collections
collections <- Zotero(
  collection.names = "The",
  case.insensitive = TRUE,
  recursive = TRUE,
  library = TRUE,
  silent = TRUE
)

# print collections
collections$collections |>
  dplyr::select(c(key, name, parentCollection))
#> # A tibble: 9 × 3
#>   key      name  parentCollection
#>   <chr>    <chr> <chr>           
#> 1 P4527NK3 over  ZYKUUCS3        
#> 2 N8Y7ANBY brown GE36M75R        
#> 3 GE36M75R quick PDX5A66Q        
#> 4 Y6CZ8PQ9 dog   6DIEZZEJ        
#> 5 6DIEZZEJ lazy  2UZ8D3SF        
#> 6 2UZ8D3SF the   P4527NK3        
#> 7 ZYKUUCS3 jumps INX272R9        
#> 8 INX272R9 fox   N8Y7ANBY        
#> 9 PDX5A66Q The   FALSE

Recursive path

# Find collections
recursive <- Zotero(
  collection.names = c("The", "quick", "brown"),
  recursive = TRUE,
  library = TRUE,
  silent = TRUE
)

# print collections
recursive$collections |>
  dplyr::select(name)
#> # A tibble: 7 × 1
#>   name 
#>   <chr>
#> 1 over 
#> 2 brown
#> 3 dog  
#> 4 lazy 
#> 5 the  
#> 6 jumps
#> 7 fox

Ancestor path

# Find collections
ancestor <- Zotero(
  collection.names = c("The", "quick", "brown", "fox"),
  ancestor = TRUE,
  library = TRUE,
  silent = TRUE
)

# print collections
ancestor$collections |>
  dplyr::select(name)
#> # A tibble: 4 × 1
#>   name 
#>   <chr>
#> 1 brown
#> 2 quick
#> 3 fox  
#> 4 The

Items

Okay, so we’ve created a bunch of collections. Kinda cool, I guess? But collections are somewhat pointless without items. Sooo, lets add some items!

Adding items from Cristin

One motivation to develop c2z is an attempt at making registering data at Cristin feel useful. The following example of the Cristin function will gather all publications containing the word “cheese”, published since 2020 (it was a good year for cheese, probably).

The zotero.import is set to TRUE, indicating that the function will use CristinWrangler to convert Cristin metadata into an acceptable format for Zotero. use.identifiers = TRUE and the function will therefore use any identifiers (i.e., ISBN or DOI) to augment the metadata. The items are posted to the Zotero library using Zotero, where items are uploaded using the items argument. The index = TRUE, creating an index of the items.

Rather than finicking over h-indexes and impact factors, Norwegians have a weird obsession with NVI (Norwegian Science Index), which is a two-level index where level one publications are ordinary and level two are great (it probably makes sense). However, Cristin has no simple method of filtering out publications that qualify for the two levels.

The specified filter arguments try to filter out categories that usually contain publications that are worthy of the NVI (see all the Cristin categories).

Let’s post the results to the collection “fox”, while using the argument get.items = FALSE as not to add any items in the collection to the Zotero list.

# Filter items
cristin.filter <- c(
  "ACADEMICREVIEW",
  "ARTICLEJOURNAL",
  "ARTICLE", 
  "ANTHOLOGYACA", 
  "CHAPTER", 
  "CHAPTERACADEMIC",
  "CHAPTERARTICLE",
  "COMMENTARYACA",
  "MONOGRAPHACA"
)

# Query Cristin
cristin <- Cristin(
  title = "cheese",
  published_since = 2020,
  published_before = 2021,
  filter = cristin.filter,
  zotero.import = TRUE,
  use.identifiers = TRUE,
  silent = FALSE
)
#> Found 15 results 
#> Checking whether references are supported. See `CristinSupported()` 
#> Looking for missing data 
#> Filtered out 9 results 
#> Sequential processing. Converting 6 items to Zotero format (began 16.03.2025 - 15:43:38) 
#> Duration: 00:01:12.498 (ended 16.03.2025 - 15:44:51)

# Get the fox key
fox <- collections$collections |> 
  dplyr::filter(name == "fox") |> 
  dplyr::pull("key")

# Post the items to the collection called fox
post.cristin <- Zotero(
  collection.key = fox,
  metadata = cristin$results,
  library = TRUE,
  index = TRUE,
  post = TRUE,
  post.collections = FALSE,
  post.items = TRUE,
  silent = TRUE
)

# Select only names in index and print
post.cristin$index |> 
  dplyr::select(name) |>
  print(width = 80)
#> # A tibble: 6 × 1
#>   name                                                                          
#>   <chr>                                                                         
#> 1 Lundberg et al. (2021) Determination of maintenance Jarlsberg® cheese dose to…
#> 2 Olsen et al. (2021) Feeding concentrates with different protein sources to hi…
#> 3 Lundberg et al. (2020) Increased serum osteocalcin levels and vitamin K statu…
#> 4 Henriquez Parodi et al. (2021) Kavli Selling Cheese in a Tube to the World    
#> 5 Gaber et al. (2021) Manufacture and characterization of acid-coagulated fresh…
#> 6 Wolka et al. (2021) Soil organic carbon and associated soil properties in Ens…

In this second example we have decided that 2019 was an even better year for cheese and change the published_since argument to 2019. The reason why we are doing this is to demonstrate the duplicate function in Cristin, which will identify any Cristin references imported into Zotero if zotero.check = TRUE. The function will try to use the zotero argument to search the specified collection(s).

# Query Cristin (again)
cristin2 <- Cristin(
  title = "cheese",
  published_since = 2019,
  published_before = 2021,
  filter = cristin.filter,
  zotero.import = TRUE,
  zotero.check = TRUE,
  use.identifiers = FALSE,
  zotero = post.cristin
)
#> Found 21 results 
#> Checking whether references exist in library 
#> Removed 6 duplicates 
#> Checking whether references are supported. See `CristinSupported()` 
#> Looking for missing data 
#> Filtered out 9 results 
#> Sequential processing. Converting 6 items to Zotero format (began 16.03.2025 - 15:44:53) 
#> Duration: 00:00:02.116 (ended 16.03.2025 - 15:44:55)

Adding items from identifiers

Sometimes, it happens to everybody, we’re having a bunch of ISBNs and DOIs lying around. Luckily, we can easily add them to Zotero using the Zotero wrapper. In this case we are adding them to the collection “quick”.

# ISBN
isbn.items <- c("9780761973836", "9788215048451")

# DOI
doi.items <- c("https://doi.org/10.31234/osf.io/venu6", 
               "10.1177/1098214010376532")

# Post the items to the collections called quick
identifiers <- Zotero(
  collection.names = c("The", "quick"),
  isbn = isbn.items,
  doi = doi.items,
  library = TRUE,
  index = TRUE,
  post = TRUE,
  post.collections = FALSE,
  post.items = TRUE,
  post.token = FALSE,
  silent = TRUE,
  get.items = FALSE
)

# Select only names in index and print
identifiers$index |> 
  dplyr::select(name) |>
  print(width = 80)
#> # A tibble: 4 × 1
#>   name                                                                          
#>   <chr>                                                                         
#> 1 Ong-Dean et al. (2011) Challenges and Dilemmas in Implementing Random Assignm…
#> 2 Wijeakumar et al. (2020) Home assessment of visual working memory in pre-scho…
#> 3 Field & Hole (2003) How to design and report experiments                      
#> 4 Grøholt et al. (2022) Lærebok i barne- og ungdomspsykiatri

Adding items from the Man

When working in academia within a Norwegian context, you will frequently need to examine the musings of politicians, or their selected group of researchers. So, let’s add some random white papers and official reports using ZoteroGov.

# Combine to a single tibble using dplyr
gov.items <- dplyr::bind_rows(
  # Find some random white papers
  ZoteroGov(c("26 (2001-2002)", "31 (2014-2015)"), type = "meldst")$data,
  # Finds some random official Norwegian reports.
  ZoteroGov(c("2014: 4", "2018: 2"), type = "nou")$data
)

# Post the items to the collection called brown
## Fitting given the source
the.man <- Zotero(
  collection.names = c("The", "quick", "brown"),
  metadata = gov.items,
  library = TRUE,
  index  = TRUE,
  post = TRUE,
  post.collections = FALSE,
  post.items = TRUE,
  post.token = FALSE,
  silent = TRUE,
  get.items = FALSE
)

# Select only names in index and print
the.man$index |> 
  dplyr::select(name) |>
  print(width = 80)
#> # A tibble: 4 × 1
#>   name                                                           
#>   <chr>                                                          
#> 1 St.meld. nr. 26 (2001-2002) Bedre kollektivtransport           
#> 2 NOU 2014: 4 (2014) Enklere regler – bedre anskaffelser         
#> 3 NOU 2018: 2 (2018) Fremtidige kompetansebehov I                
#> 4 Meld. St. 31 (2014–2015) Garden som ressurs – marknaden som mål

Adding items from CRAN

Show some love (citations are love, yes?) for the authors that create the packages that you use! (We see you, Hadley Wickham).

Here, we’re using the ZoteroCran function to collect metadata from The Comprehensive R Archive Network (CRAN). The collection.key is defined as:

lazy <- collections$collections |> 
  dplyr::filter(name == "lazy") |> 
  dplyr::pull("key")

The key is “6DIEZZEJ” and the argument recursive = TRUE, thus the function is looking for the specified collection (i.e., “lazy”) and all its children (i.e., “dog”).

It is often useful to link items to several (nested) collections. For instance, you can create the nested collections “project -> CRAN-packages” and link the items to both collections. You can then choose to access all items under “project”, including those in “CRAN-packages”, or just access those filed under the latter.

# Find selected packages 
packages <- c(
  "dplyr",
  "httr",
  "jsonlite",
  "purrr",
  "rvest",
  "rlang",
  "tibble",
  "tidyr",
  "tidyselect"
)

# Post the items to the collections called lazy and dog
cran <- Zotero(
  collection.key = lazy,
  metadata = ZoteroCran(packages)$data,
  library = TRUE,
  index = TRUE,
  recursive = TRUE,
  post = TRUE,
  post.collections = FALSE,
  post.items = TRUE,
  post.token = FALSE,
  silent = FALSE,
  get.items = FALSE
)
#> Searching for collections 
#> Found 9 collections 
#> The Zotero list contains: 2 collections, 0 items, and 0 attachments 
#> Adding 9 items to library using 1 POST request 
#> —————————————————Process: 100.00% (1/1). Elapsed time: 00:00:00—————————————————
#> $post.status.items
#> # A tibble: 9 × 2
#>   status  key     
#>   <fct>   <chr>   
#> 1 success 2UZHXDE9
#> 2 success 2LJFGM78
#> 3 success B8CERAIB
#> 4 success FEB6YC86
#> 5 success 7GU66E2M
#> 6 success AS5CYNSX
#> 7 success YZYRLKA2
#> 8 success HNB7946R
#> 9 success NVSIAN7I
#> 
#> $post.summary.items
#> # A tibble: 1 × 2
#>   status  summary
#>   <fct>     <int>
#> 1 success       9
#> 
#> 
#> Creating index for items

# What do we got?
# Select only names in index and print
index <- cran$index |> 
  dplyr::select(name) |>
  print(width = 80)
#> # A tibble: 9 × 1
#>   name                                                                          
#>   <chr>                                                                         
#> 1 Wickham (2023a) dplyr: A Grammar of Data Manipulation                         
#> 2 Wickham (2023b) httr: Tools for Working with URLs and HTTP                    
#> 3 Ooms (2025) jsonlite: A Simple and Robust JSON Parser and Generator for R     
#> 4 Wickham (2025) purrr: Functional Programming Tools                            
#> 5 Henry (2025) rlang: Functions for Base Types and Core R and 'Tidyverse' Featu…
#> 6 Wickham (2024a) rvest: Easily Harvest (Scrape) Web Pages                      
#> 7 Müller (2023) tibble: Simple Data Frames                                      
#> 8 Wickham (2024b) tidyr: Tidy Messy Data                                        
#> 9 Henry (2024) tidyselect: Select from a Set of Strings

Copying

There are many situations where we want to copy collections and items (and attachments) between libraries. You may want to copy from a public Zotero library to your own or share your collections with a research group.

The Zotero function contains a (somewhat convoluted) method of copying collections and items from a group library to a user library (and vice versa). However, in this example we’re appending the group collections to our long line of nested collections, making the process somewhat messy. Therefore, we split up the process using the ZoteroCopy and ZoteroPost functions. The change.library argument will query Zotero and alter the location according to specified coordinates (the default location is the user library as defined in .Renviron).

Please see the tutorial on how to set up user/group id and API keys.

# Access the group library (defined in .Renviron)
group.library <- Zotero(
  user = FALSE,
  library = TRUE,
  silent = FALSE
)
#> Searching for collections 
#> Found 5 collections 
#> The Zotero list contains: 5 collections, 0 items, and 0 attachments 
#> Searching for all items in library 
#> Found 3 items 
#> The Zotero list contains: 5 collections, 3 items, and 0 attachments

# Copy the library creating new keys 
copy.library <- ZoteroCopy(
  zotero = group.library,
  change.library = TRUE,
  silent = FALSE
)
#> Copying collections 
#> Copying items

# Find key for dog
dog <- collections$collections |> 
  dplyr::filter(name == "dog") |> 
  dplyr::pull("key")

# Change parent collection of top-level collection to dog
copy.library$collections <- copy.library$collections |>
  dplyr::mutate(
    parentCollection = dplyr::case_when(
      parentCollection == "FALSE" ~ dog,
      TRUE ~ parentCollection
    )
  )

# Copy the collection and items to the user library
post.copy <- ZoteroPost(
  Zotero(
    collections = copy.library$collections,
    items = copy.library$items
  ),
  silent = FALSE,
  post.token = TRUE
)
#> Adding 5 collections to library using 1 POST request 
#> NULL
#> 
#> Adding 3 items to library using 1 POST request 
#> —————————————————Process: 100.00% (1/1). Elapsed time: 00:00:00—————————————————
#> $post.status.items
#> # A tibble: 3 × 3
#>   error                                    status key     
#>   <chr>                                    <fct>  <chr>   
#> 1 Error 409: Collection DQDXE9GT not found failed A88L8M8T
#> 2 Error 409: Collection DQDXE9GT not found failed M3Q65ZPZ
#> 3 Error 409: Collection DQDXE9GT not found failed NPKPIBVY
#> 
#> $post.summary.items
#> # A tibble: 1 × 2
#>   status summary
#>   <fct>    <int>
#> 1 failed       3


if (any(nrow(post.copy$items))) {
  # Select only names in index and print
  ZoteroIndex(post.copy$items) |>
    dplyr::select(name) |>
    print(width = 80)
}
#> # A tibble: 3 × 1
#>   name                                                                          
#>   <chr>                                                                         
#> 1 Mishra et al. (2023) Exploring Active and Critical Engagement in Human-Robot …
#> 2 Somby & Stalheim (2023) Vygotsky med VR-briller                               
#> 3 Somby & Vik (2023) Vygotskys defektologi - et perspektiv på inkluderende oppl…

Bibliography

It’s a wrap!

Now it’s time to harvest the bounty that we have created during the steps above. We could create separate bibliographies, but for the sake of simplicity we are mashing it all together. That’s friendship!

The arguments used in Zotero are somewhat self-explanatory, however, we are also csl.type to access a style repository in order to create a Citation Style Language (CSL) file according to APA7.

# Create references.bib in biblatex format with style.csl according to APA7
bibliography <- Zotero(
  collection.names = "The",
  recursive = TRUE,
  library = TRUE,
  export = TRUE,
  format = "biblatex",
  save.data = TRUE,
  save.path = tempdir(),
  bib.name = "references",
  csl.type = "apa-single-spaced",
  csl.name = "style",
  silent = TRUE
)

# What do we got?
sprintf(
  "We now have %s collections, %s references, and %s attachments",
  bibliography$n.collections,
  bibliography$n.items,
  bibliography$n.attachments
)
#> [1] "We now have 9 collections, 23 references, and 0 attachments"

Deleting

Housecleaning! We should clean up the mess in your library. You could set ragnarok and force = TRUE, if you want to delete everything in your library.

# Delete all collections and items belonging to initial key
delete <- ZoteroDelete(
  bibliography, 
  delete.collections = TRUE,
  delete.items = TRUE
)
#> Deleting 9 collections using 1 DELETE request 
#> —————————————————Process: 100.00% (1/1). Elapsed time: 00:00:00—————————————————
#> Deleting 23 items using 1 DELETE request 
#> —————————————————Process: 100.00% (1/1). Elapsed time: 00:00:00—————————————————

References

We can now display the references that we have collected and created using c2z.

Field, A. P., & Hole, G. (2003). How to design and report experiments. Sage publications Ltd.
Gaber, S. M., Johansen, A.-G., Devold, T. G., Rukke, E.-O., & Skeie, S. B. (2021). Manufacture and characterization of acid-coagulated fresh cheese made from casein concentrates obtained by acid diafiltration. Journal of Dairy Science, 104(6), 6598–6608. https://doi.org/10.3168/jds.2020-19917
Grøholt, B., Weidle, B., Garløv, I., & Ramleth, R.-K. (2022). Lærebok i barne- og ungdomspsykiatri (6th ed.). Universitetsforlaget.
Henriquez Parodi, M. C., Lankut, E., & Alon, I. (2021). Kavli selling cheese in a tube to the world. In M. Chavan & L. Taksa (Eds.), Intercultural management in practice: Learning to lead diverse global organizations (p. 27). Emerald Publishing. https://doi.org/https://doi.org/10.1108/978-1-83982-826-320211004
Henry, L. (2024). Tidyselect: Select from a set of strings (Version 1.2.1) [Computer software]. https://cran.r-project.org/package=tidyselect
Henry, L. (2025). Rlang: Functions for base types and core r and ’tidyverse’ features (Version 1.1.5) [Computer software]. https://cran.r-project.org/package=rlang
Lundberg, H. E., Holand, T., Holo, H., & Larsen, S. (2020). Increased serum osteocalcin levels and vitamin k status by daily cheese intake. International Journal of Clinical Trials, 7(2), 55. https://doi.org/10.18203/2349-3259.ijct20201712
Lundberg, H. E., Holo, H., Holand, T., Fagertun, H. E., & Larsen, S. (2021). Determination of maintenance jarlsberg® cheese dose to keep the obtained serum osteocalcin level; a response surface pathway designed de-escalation dose study with individual starting values. International Journal of Clinical Trials (IJCT), 8(3), 174. https://doi.org/https://doi.org/10.18203/2349-3259.ijct20212841
Meld. St. 31 (2014–2015). (2015). Garden som ressurs – marknaden som mål. Landbruks- og matdepartementet. https://www.regjeringen.no/id2415017
Müller, K. (2023). Tibble: Simple data frames (Version 3.2.1) [Computer software]. https://cran.r-project.org/package=tibble
NOU 2014: 4. (2014). Enklere regler – bedre anskaffelser. Nærings- og fiskeridepartementet. https://www.regjeringen.no/id761768
NOU 2018: 2. (2018). Fremtidige kompetansebehov I. Kunnskapsdepartementet. https://www.regjeringen.no/id2588070
Olsen, M. A., Vhile, S. G., Porcellato, D., Kidane, A., & Skeie, S. B. (2021). Feeding concentrates with different protein sources to high-yielding, mid-lactation norwegian red cows: Effect on cheese ripening. Journal of Dairy Science, 104(4), 4062–4073. https://doi.org/10.3168/jds.2020-19226
Ong-Dean, C., Huie Hofstetter, C., & Strick, B. R. (2011). Challenges and dilemmas in implementing random assignment in educational research. American Journal of Evaluation, 32(1), 29–49. https://doi.org/10.1177/1098214010376532
Ooms, J. (2025). Jsonlite: A simple and robust JSON parser and generator for r (Version 1.9.1) [Computer software]. https://cran.r-project.org/package=jsonlite
St.meld. nr. 26 (2001-2002). (2002). Bedre kollektivtransport. Samferdselsdepartementet. https://www.regjeringen.no/id196149
Wickham, H. (2023a). Httr: Tools for working with URLs and HTTP (Version 1.4.7) [Computer software]. https://cran.r-project.org/package=httr
Wickham, H. (2023b). Dplyr: A grammar of data manipulation (Version 1.1.4) [Computer software]. https://cran.r-project.org/package=dplyr
Wickham, H. (2024a). Tidyr: Tidy messy data (Version 1.3.1) [Computer software]. https://cran.r-project.org/package=tidyr
Wickham, H. (2024b). Rvest: Easily harvest (scrape) web pages (Version 1.0.4) [Computer software]. https://cran.r-project.org/package=rvest
Wickham, H. (2025). Purrr: Functional programming tools (Version 1.0.4) [Computer software]. https://cran.r-project.org/package=purrr
Wijeakumar, S., Rafetseder, E., Shing, Y. L., & McKay, C. (2020). Home assessment of visual working memory in pre-schoolers reveals associations between behaviour, brain activation and environmental measures. PsyArXiv. https://doi.org/10.31234/osf.io/venu6
Wolka, K., Biazin, B., Martinsen, V., & Mulder, J. (2021). Soil organic carbon and associated soil properties in enset (ensete ventricosum welw. Cheesman)-based homegardens in ethiopia. Soil and Tillage Research, 205(2021), 104791. https://doi.org/10.1016/j.still.2020.104791