<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Evgene</title>
    <description>The latest articles on DEV Community by Evgene (@evgenekopylov).</description>
    <link>https://dev.to/evgenekopylov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3940739%2Fd91c060f-dc13-4be8-b120-a02f3cf50831.png</url>
      <title>DEV Community: Evgene</title>
      <link>https://dev.to/evgenekopylov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/evgenekopylov"/>
    <language>en</language>
    <item>
      <title>SQL-like Queries in FSRS Plugin for Obsidian</title>
      <dc:creator>Evgene</dc:creator>
      <pubDate>Sat, 30 May 2026 21:38:10 +0000</pubDate>
      <link>https://dev.to/evgenekopylov/sql-like-queries-in-fsrs-plugin-for-obsidian-5ac8</link>
      <guid>https://dev.to/evgenekopylov/sql-like-queries-in-fsrs-plugin-for-obsidian-5ac8</guid>
      <description>&lt;h1&gt;
  
  
  SQL-like Queries in FSRS Plugin for Obsidian
&lt;/h1&gt;

&lt;p&gt;Spaced repetition in Obsidian usually works as "show all cards with due earlier than today." That's enough for simple cases, but once you have hundreds of notes, you want to filter, sort, and select.&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://community.obsidian.md/plugins/fsrs" rel="noopener noreferrer"&gt;FSRS&lt;/a&gt; plugin now has a query language resembling SQL. It turns a markdown block into a live table that updates with every review.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;fsrs-table
&lt;/span&gt;&lt;span class="sb"&gt;SELECT file as "Note",
       r as "Retrievability",
       date_format(due, '%d.%m.%Y') as "Due"
WHERE r &amp;lt; 0.7
ORDER BY r ASC
LIMIT 20&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ the table shows the 20 most "forgotten" cards, sorted by retrieval probability.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg97kez76wo57o7lbd1g8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg97kez76wo57o7lbd1g8.png" alt="table render" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  From Simple Settings to an Embedded DB
&lt;/h2&gt;

&lt;p&gt;Initially I planned to offer table settings using standard SQL syntax. But pretty quickly the syntax became a real query language, and the implementation itself — an embedded lightweight DB.&lt;/p&gt;

&lt;p&gt;High-level test coverage in TypeScript made it easy to iterate on functionality located in the WASM module via an AI agent.&lt;br&gt;
When faced with dual-language testing (TypeScript + Rust), the artificial intelligence prefers to do the job properly rather than fake it.&lt;/p&gt;

&lt;p&gt;After implementing the &lt;strong&gt;lexer → parser → AST → evaluator&lt;/strong&gt; pipeline for numeric values, I extended it to strings, added filtering via WHERE, then functions.&lt;br&gt;
Extending the syntax or adding a function came down to a single request to the agent — and a feasibility check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Inside &lt;code&gt;fsrs-table&lt;/code&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Supported Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;/strong&gt; — choose fields, rename via &lt;code&gt;AS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHERE&lt;/code&gt;&lt;/strong&gt; — conditions with &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;!=&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;=&lt;/code&gt;, &lt;code&gt;&amp;gt;=&lt;/code&gt;, &lt;code&gt;AND&lt;/code&gt;, &lt;code&gt;OR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ORDER BY&lt;/code&gt;&lt;/strong&gt; — sort ascending (&lt;code&gt;ASC&lt;/code&gt;) or descending (&lt;code&gt;DESC&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LIMIT&lt;/code&gt;&lt;/strong&gt; — cap the number of rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;date_format()&lt;/code&gt;&lt;/strong&gt; — convert the &lt;code&gt;due&lt;/code&gt; date to any text format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Available fields:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field (alias)&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;path to the note&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;due&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;date&lt;/td&gt;
&lt;td&gt;next review date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;stability&lt;/code&gt; (s)&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;stability in days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;difficulty&lt;/code&gt; (d)&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;difficulty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;retrievability&lt;/code&gt; (r)&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;probability of recall (0…1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;total number of reviews&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;New, Learning, Review, or Relearning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;elapsed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;days since last review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scheduled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;scheduled interval in days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What &lt;code&gt;fsrs-table&lt;/code&gt; Can't Do (and Shouldn't)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Subqueries, &lt;code&gt;JOIN&lt;/code&gt;, aggregations (&lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;SUM&lt;/code&gt;…).&lt;/li&gt;
&lt;li&gt;Data modification (&lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIMIT&lt;/code&gt; doesn't short-circuit processing (to guarantee the first N rows by sort order, all cards must be evaluated).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a database — it's a filter + sort over a cached set of cards.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It's Implemented (Briefly)
&lt;/h2&gt;

&lt;p&gt;All query processing happens inside &lt;strong&gt;Rust/WASM&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lexer&lt;/strong&gt; turns the query string into tokens (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;LIMIT&lt;/code&gt;, identifiers, operators).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parser&lt;/strong&gt; builds an AST (abstract syntax tree) respecting operator precedence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluator&lt;/strong&gt; walks the AST for each card and checks the condition.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// simplified: WHERE clause AST&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Comparison&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ComparisonOp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;Logical&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LogicalOp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// AND or OR&lt;/span&gt;
        &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser is hand-written (not &lt;code&gt;nom&lt;/code&gt;/&lt;code&gt;pest&lt;/code&gt;) to keep full control over error messages. On an invalid query, the plugin shows a readable message: "Unknown field: retriv".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not SQLite?&lt;/strong&gt;&lt;br&gt;
SQLite would require WASM compilation (maybe possible) and an extra synchronization layer. My implementation is lighter, needs no external dependencies, and works exclusively with data already loaded in memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;The card cache lives inside WASM. On the first vault scan, the plugin computes &lt;code&gt;stability&lt;/code&gt;, &lt;code&gt;difficulty&lt;/code&gt;, &lt;code&gt;due&lt;/code&gt;, and &lt;code&gt;retrievability&lt;/code&gt; for each card. Subsequent queries work off this cache.&lt;/p&gt;

&lt;p&gt;On a vault with &lt;strong&gt;5,000&lt;/strong&gt; cards, end-to-end from UI action to displayed table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full scan + condition evaluation for all cards takes &lt;strong&gt;0.07 s&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Sorting by &lt;code&gt;r&lt;/code&gt; — another &lt;strong&gt;0.02 s&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIMIT&lt;/code&gt; adds no gain, but 0.07 s is imperceptible to the user anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All fields (&lt;code&gt;stability&lt;/code&gt;, &lt;code&gt;difficulty&lt;/code&gt;, &lt;code&gt;retrievability&lt;/code&gt;) are computed on the fly from review history (stored in YAML frontmatter). Each answer recalculates only one card — cost &amp;lt; 0.01 s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Query Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Review what's about to be forgotten
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;"Probability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date_format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;due&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%d.%m'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Drill the hardest cards
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;"Difficulty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;"Stability (days)"&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"Review"&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Overdue cards (due in the past)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date_format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;due&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%d.%m.%Y'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;due&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-01_00:00'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;due&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  New cards only
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reps&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"New"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The realization that a table configuration method had turned into a full-fledged embedded database didn't come right away. Which suggests that's how the first DBs came to be — out of a need to solve simple practical problems.&lt;/p&gt;

&lt;p&gt;The plugin is already available in the Obsidian community catalog. Install it, try it out, and write your own queries.&lt;/p&gt;

&lt;p&gt;Or clone the plugin &lt;a href="https://github.com/Evgene-Kopylov/fsrs_plugin" rel="noopener noreferrer"&gt;repository&lt;/a&gt; and check if you really can extend the SQL functionality with a single prompt to an agent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/evgenekopylov/fsrs-for-obsidian-remember-what-matters-2l9i"&gt;FSRS for Obsidian: Remember Everything&lt;/a&gt; — why the plugin exists and how it works&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/evgenekopylov/fsrs-plugin-for-obsidian-rustwasm-architecture-and-performance-3kho"&gt;FSRS Plugin: Rust/WASM Architecture and Performance&lt;/a&gt; — technical deep-dive into the architecture&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Evgene Kopylov, 2026&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>showdev</category>
      <category>sql</category>
      <category>rust</category>
    </item>
    <item>
      <title>FSRS Plugin for Obsidian: Rust/WASM Architecture and Performance</title>
      <dc:creator>Evgene</dc:creator>
      <pubDate>Sat, 30 May 2026 21:35:27 +0000</pubDate>
      <link>https://dev.to/evgenekopylov/fsrs-plugin-for-obsidian-rustwasm-architecture-and-performance-3kho</link>
      <guid>https://dev.to/evgenekopylov/fsrs-plugin-for-obsidian-rustwasm-architecture-and-performance-3kho</guid>
      <description>&lt;h1&gt;
  
  
  FSRS Plugin for Obsidian: Rust/WASM Architecture and Performance
&lt;/h1&gt;

&lt;p&gt;A spaced repetition tool for Obsidian notes must use a modern algorithm and work locally with notes as-is (without rewriting them into flashcards).&lt;/p&gt;

&lt;p&gt;Existing Obsidian plugins stop at the SM-2 algorithm circa 1987.&lt;/p&gt;

&lt;p&gt;Alternative solutions exist "somewhere else" — outside free software, outside Markdown-first architecture — tied to the cloud or a proprietary format.&lt;/p&gt;

&lt;p&gt;I wrote my own because I couldn't find a suitable one.&lt;/p&gt;

&lt;p&gt;FSRS, a computational core in Rust compiled to WebAssembly.&lt;/p&gt;

&lt;p&gt;This article covers: WebAssembly architecture, a custom parser, a lexer, and performance benchmarks. Every query runs in hundredths of a second. Blazingly fast 🦀&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a technical article. For a step-by-step user guide, see the &lt;a href="https://dev.to/evgenekopylov/fsrs-for-obsidian-remember-what-matters-2l9i"&gt;overview article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb80fank34k94g0agxfrw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb80fank34k94g0agxfrw.gif" alt="plugin demo: card table and popup preview" width="720" height="390"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a Fourth Spaced Repetition Plugin?
&lt;/h2&gt;

&lt;p&gt;At the time of writing, three popular solutions exist in Obsidian: &lt;code&gt;obsidian-spaced-repetition&lt;/code&gt;, &lt;code&gt;obsidian-recall&lt;/code&gt;, and &lt;code&gt;obsidian-review&lt;/code&gt;. All use SM-2 — an algorithm nearly 40 years old. It works, but requires roughly 30% more reviews than FSRS for the same retention level.&lt;/p&gt;

&lt;p&gt;The main drawbacks of SM-2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same interval regardless of material difficulty&lt;/li&gt;
&lt;li&gt;Doesn't handle breaks — resets progress after a gap&lt;/li&gt;
&lt;li&gt;No concept of retrievability — the probability of recalling a card right now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FSRS is a step forward. But it wasn't in Obsidian. Until now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5brm4btgc1sz0wejz9i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5brm4btgc1sz0wejz9i.png" alt="sm-2-vs-fsrs" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How FSRS Works in a Nutshell
&lt;/h2&gt;

&lt;p&gt;FSRS operates on a DSR model with three parameters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Difficulty&lt;/strong&gt; — how hard the material is. Range: 0–10&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stability&lt;/strong&gt; — memory strength in days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrievability&lt;/strong&gt; — probability of recalling the card right now&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After each answer (Again / Hard / Good / Easy), the algorithm recalculates difficulty and stability. Retrievability changes continuously.&lt;/p&gt;

&lt;p&gt;The algorithm uses 21 parameters, tuned by machine learning on millions of real reviews.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68yizilkq1gcc1eejnql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68yizilkq1gcc1eejnql.png" alt="DSR-schema" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Why Rust and WebAssembly
&lt;/h2&gt;

&lt;p&gt;An Obsidian plugin is JavaScript. But FSRS requires precise floating-point calculations on every review.&lt;/p&gt;

&lt;p&gt;I chose Rust for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; — WASM runs orders of magnitude faster than JS on numerical computations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ecosystem&lt;/strong&gt; — the &lt;code&gt;rs-fsrs&lt;/code&gt; crate from the open-spaced-repetition community, providing the reference FSRS implementation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type safety&lt;/strong&gt; — in Rust you can't accidentally mix up &lt;code&gt;difficulty&lt;/code&gt; and &lt;code&gt;stability&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Separation of concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeScript                  Rust/WASM
─────────                   ─────────
• Obsidian API              • FSRS computations
• UI / rendering            • SQL-like syntax parsing
• File system               • YAML/JSON parsing
• Plugin lifecycle          • Filtering and sorting
• Buttons, modals           • Card cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqlrlni74m8n2c5be7aw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqlrlni74m8n2c5be7aw.png" alt="fsrs-plugin-schema" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;TypeScript is a thin wrapper over the Obsidian API. All logic lives in WASM.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance: The WASM ⇆ JS Boundary
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Minimizing Cross-Boundary Copying
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cache inside WASM&lt;/strong&gt; — filtering and sorting happen right there in Rust. Only the result (20–200 rows) crosses the boundary, not all 10,000 cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental updates&lt;/strong&gt; — answering a card recalculates only one record.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  metadataCache: Don't Read Files
&lt;/h3&gt;

&lt;p&gt;The biggest speed gain comes from not reading files. Obsidian stores parsed frontmatter in &lt;code&gt;metadataCache&lt;/code&gt; — an in-memory cache that updates on every note change.&lt;/p&gt;

&lt;p&gt;The plugin checks for FSRS fields via &lt;code&gt;metadataCache.getFileCache()&lt;/code&gt; — instant, in-memory access, no I/O. Out of 105,607 files, only those whose frontmatter already contains &lt;code&gt;reviews&lt;/code&gt; are actually read.&lt;/p&gt;

&lt;p&gt;For reference: the plugin's own parser processes 105k files in 16 seconds, filters out 100k, and processes 5k.&lt;/p&gt;

&lt;p&gt;Obsidian spends ~20 minutes indexing 105k files,&lt;br&gt;
and ~120 seconds on 5,000 freshly added cards.&lt;br&gt;
So the fair comparison is 16 seconds for the plugin vs. 120 for Obsidian.&lt;br&gt;
But Obsidian does it anyway — so it's more efficient to use the ready-made cache.&lt;/p&gt;
&lt;h3&gt;
  
  
  Numbers
&lt;/h3&gt;

&lt;p&gt;FSRS calculation for all cards runs &lt;strong&gt;once&lt;/strong&gt; during the initial&lt;br&gt;
vault scan. After that, the cache lives in WASM — all subsequent&lt;br&gt;
operations (table load, heatmap, single card update) work with&lt;br&gt;
pre-computed data and don't depend on vault size.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Large vault (105k files, ~5,000 cards)&lt;/th&gt;
&lt;th&gt;Small vault (710 files, 104 cards)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Initial scan&lt;/strong&gt; (FSRS for all cards)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.2 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.04 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Table load&lt;/strong&gt; (after cache)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.07 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.04 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Heatmap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.02 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.01 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Single card update&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 0.01 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 0.01 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference between 5,000 and 100 cards after caching — &lt;strong&gt;0.03 s&lt;/strong&gt;. &lt;a href="https://gitlab.com/Evgene-Kopylov/fsrs-plugin-articles/-/raw/main/materials/performance-0.11.10-large-vault.txt" rel="noopener noreferrer"&gt;Large vault logs&lt;/a&gt;, &lt;a href="https://gitlab.com/Evgene-Kopylov/fsrs-plugin-articles/-/raw/main/materials/performance-0.11.10-small-vault.txt" rel="noopener noreferrer"&gt;Small vault logs with plugins&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every plugin action, after the initial calculation, runs in hundredths of a second.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  The Bottleneck
&lt;/h3&gt;

&lt;p&gt;3.2 seconds for initial load — that's the FSRS calculation for each of 5,000 cards. Runs only once.&lt;/p&gt;

&lt;p&gt;States could be persisted to disk cache, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syncing complexity between devices (on-disk cache can go stale)&lt;/li&gt;
&lt;li&gt;5,000 cards / 3.2 seconds — acceptable for real-world use&lt;/li&gt;
&lt;li&gt;After the first launch, subsequent plugin loads are instant — the cache is already in WASM&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  What's Wrong (About Trade-offs)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;LIMIT&lt;/code&gt; in the current implementation doesn't short-circuit processing — to guarantee the first N rows by retrievability, all cards must still be evaluated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A deliberate trade-off:&lt;/strong&gt; a real user's vault rarely exceeds 5,000–10,000 entries; a full scan + sort takes 0.005–0.010 s.&lt;/p&gt;


&lt;h2&gt;
  
  
  WASM Cache Instead of Local State
&lt;/h2&gt;

&lt;p&gt;The entire cache (&lt;code&gt;HashMap&amp;lt;filePath, CachedCard&amp;gt;&lt;/code&gt;) lives inside WASM as a global variable. The plugin stores no state in TypeScript at all.&lt;/p&gt;

&lt;p&gt;Why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single source of truth&lt;/strong&gt; — no desync between JS state and WASM computations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast queries&lt;/strong&gt; — filtering/sorting happen right where the data is&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental updates&lt;/strong&gt; — targeted commands: "update this card" / "delete this one"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where data lives.&lt;/strong&gt; Review progress is stored directly in a note's YAML frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;reviews&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-05-03T12:00:00Z"&lt;/span&gt;
    &lt;span class="na"&gt;rating&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-05-04T08:30:00Z"&lt;/span&gt;
    &lt;span class="na"&gt;rating&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;due&lt;/code&gt;, &lt;code&gt;stability&lt;/code&gt;, &lt;code&gt;difficulty&lt;/code&gt;, and &lt;code&gt;state&lt;/code&gt; are not stored — the WASM core computes them on the fly from the review history.&lt;/p&gt;




&lt;h2&gt;
  
  
  SQL-like Language for Tables
&lt;/h2&gt;

&lt;p&gt;The plugin's headline feature — selecting cards for review via an &lt;code&gt;fsrs-table&lt;/code&gt; block.&lt;br&gt;
You write a SQL-like query in a markdown note and get a live table&lt;br&gt;
that auto-updates with every review.&lt;/p&gt;

&lt;p&gt;Under the hood: a custom parser built from scratch — &lt;strong&gt;lexer → parser → AST → evaluator&lt;/strong&gt;&lt;br&gt;
in Rust/WASM. Supports &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;code&gt;LIMIT&lt;/code&gt;,&lt;br&gt;
and the &lt;code&gt;date_format()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Full breakdown in a dedicated article:&lt;br&gt;
&lt;a href="https://dev.to/evgenekopylov/sql-like-queries-in-fsrs-plugin-for-obsidian-5ac8"&gt;SQL-like Queries in FSRS Plugin&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  No Mocks as a Consequence of Architecture
&lt;/h2&gt;

&lt;p&gt;Mocks aren't needed — not because they're "forbidden," but because there's nothing to mock.&lt;/p&gt;

&lt;p&gt;TypeScript in the plugin is a thin wrapper over the Obsidian API. All logic is in Rust/WASM. Mocking Obsidian would mean checking whether the plugin closes a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, or whether it called &lt;code&gt;vault.read()&lt;/code&gt; — trivial glue not worth testing. What's worth testing: the integration between your own TypeScript and your own WASM.&lt;/p&gt;

&lt;p&gt;So tests are split into two levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rust tests (184)&lt;/strong&gt; — pure functions, isolated from the environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript tests (86)&lt;/strong&gt; — unit tests for pure functions and TS → WASM integration tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integration tests start and end with your own code: a raw string, a parameter, or a call on the input (TS) → parsing (WASM) → WASM cache query (WASM) → result (TS).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5uh0uohsbmuj7p20ho89.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5uh0uohsbmuj7p20ho89.png" alt="tests-terminal" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  CI/CD: Build, Test, Release at the Push of a Button
&lt;/h2&gt;

&lt;p&gt;A GitLab CI pipeline with seven stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;check&lt;/code&gt; — &lt;code&gt;cargo fmt&lt;/code&gt;, &lt;code&gt;cargo clippy&lt;/code&gt;, &lt;code&gt;cargo test&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build-wasm&lt;/code&gt; — &lt;code&gt;wasm-pack build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;encode-wasm&lt;/code&gt; — embedding WASM as base64 for the bundle&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test&lt;/code&gt; — TypeScript tests (vitest)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lint&lt;/code&gt; — &lt;code&gt;tsc --noEmit&lt;/code&gt; + ESLint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build&lt;/code&gt; — final &lt;code&gt;main.js&lt;/code&gt; build&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;release&lt;/code&gt; — automatic GitHub release&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub985rsec5mxeyoz1vnm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub985rsec5mxeyoz1vnm.png" alt="ci-green" width="799" height="76"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Current Status
&lt;/h2&gt;

&lt;p&gt;The plugin is ready to use.&lt;/p&gt;

&lt;p&gt;Tested on Ubuntu, Windows, and Android.&lt;/p&gt;

&lt;p&gt;Available in the Obsidian community plugin catalog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's already done:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI/CD that builds and publishes releases automatically&lt;/li&gt;
&lt;li&gt;Transparent storage — all data in YAML frontmatter of your &lt;code&gt;.md&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;TS → WASM integration tests (raw SQL, no mocks)&lt;/li&gt;
&lt;li&gt;Heatmap&lt;/li&gt;
&lt;li&gt;Russian, English, and Chinese localization&lt;/li&gt;
&lt;li&gt;SQL-like queries for table-based card selection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Planned:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gather feedback&lt;/li&gt;
&lt;li&gt;Polish the mobile interface&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How to Install
&lt;/h2&gt;

&lt;p&gt;Available in the Obsidian community plugin catalog.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Settings → Community plugins → Browse&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Find &lt;strong&gt;FSRS&lt;/strong&gt; → &lt;strong&gt;Install&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enable the plugin in &lt;strong&gt;Settings → Community plugins&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; — Obsidian API, UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; — computational core (WASM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;esbuild&lt;/strong&gt; — JS bundle build&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;wasm-pack&lt;/strong&gt; — WASM build&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vitest&lt;/strong&gt; — TypeScript tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab CI/CD&lt;/strong&gt; — pipeline&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Evgene-Kopylov/fsrs_plugin" rel="noopener noreferrer"&gt;Repository on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/evgenekopylov/fsrs-for-obsidian-remember-what-matters-2l9i"&gt;Overview article&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/evgenekopylov/sql-like-queries-in-fsrs-plugin-for-obsidian-5ac8?preview=2ae6285ae4552a4d501f43d4e791fb8b88327ed8465d5a167fe0441bf5f4a09f610dbaad4856ef1964dacb3110d4b700c212572ffdfde65f7cc97bac"&gt;SQL-like queries&lt;/a&gt; — dedicated article&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Evgene Kopylov, 2026&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webassembly</category>
      <category>obsidian</category>
      <category>performance</category>
    </item>
    <item>
      <title>FSRS for Obsidian: remember what matters</title>
      <dc:creator>Evgene</dc:creator>
      <pubDate>Sat, 30 May 2026 21:29:32 +0000</pubDate>
      <link>https://dev.to/evgenekopylov/fsrs-for-obsidian-remember-what-matters-2l9i</link>
      <guid>https://dev.to/evgenekopylov/fsrs-for-obsidian-remember-what-matters-2l9i</guid>
      <description>&lt;h1&gt;
  
  
  FSRS for Obsidian: remember what matters
&lt;/h1&gt;

&lt;p&gt;Obsidian is called a "second brain". But to truly become one, links alone aren't enough — you need &lt;strong&gt;memory&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I built a spaced repetition plugin powered by the modern &lt;strong&gt;FSRS&lt;/strong&gt; algorithm. It remembers what you studied and when, how hard it was, and predicts your recall probability. All data stays in your &lt;code&gt;.md&lt;/code&gt; files — nothing is sent to any server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb80fank34k94g0agxfrw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb80fank34k94g0agxfrw.gif" alt="plugin demo: card table with hover preview" width="720" height="390"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why another repetition plugin?
&lt;/h2&gt;

&lt;p&gt;Because you shouldn't have to fiddle with cards when your notes are already right there.&lt;/p&gt;

&lt;h3&gt;
  
  
  SM-2 vs FSRS
&lt;/h3&gt;

&lt;p&gt;SM-2 repeats everything at the same intervals. Miss a week? Progress resets. You can calculate it on a napkin.&lt;/p&gt;

&lt;p&gt;Problems with SM-2 in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same intervals&lt;/strong&gt; for easy and hard material — you review what you know and what you don't at the same rate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progress reset&lt;/strong&gt; after a break — skipped a week? Start over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No forgetting prediction&lt;/strong&gt; — the plugin won't tell you that you're about to forget&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FSRS solves all of this. At the same retention level, it requires &lt;strong&gt;roughly 30% fewer repetitions&lt;/strong&gt; — and with FSRS-6 used in the plugin, the real-world difference is noticeably larger.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5brm4btgc1sz0wejz9i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5brm4btgc1sz0wejz9i.png" alt="SM-2 vs FSRS comparison" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  FSRS plugin vs other tools
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Obsidian + FSRS (this plugin)&lt;/th&gt;
&lt;th&gt;Anki + FSRS&lt;/th&gt;
&lt;th&gt;RemNote + FSRS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;.md&lt;/code&gt;, your files&lt;/td&gt;
&lt;td&gt;⚠️ SQLite, optional cloud&lt;/td&gt;
&lt;td&gt;⚠️ Local or cloud by choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Card context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Full note, links, graph&lt;/td&gt;
&lt;td&gt;⚠️ Snippets of text&lt;/td&gt;
&lt;td&gt;✅ Note = card&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Card creation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Note = card&lt;/td&gt;
&lt;td&gt;❌ Copy-paste, form&lt;/td&gt;
&lt;td&gt;⚠️ Quick from rem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Algorithm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ FSRS&lt;/td&gt;
&lt;td&gt;✅ FSRS&lt;/td&gt;
&lt;td&gt;✅ FSRS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Privacy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Fully offline&lt;/td&gt;
&lt;td&gt;⚠️ Local, optional account&lt;/td&gt;
&lt;td&gt;✅ Offline mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sync&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Not needed — sync your vault&lt;/td&gt;
&lt;td&gt;✅ Free via AnkiWeb (optional)&lt;/td&gt;
&lt;td&gt;❌ Pro subscription only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Fully open&lt;/td&gt;
&lt;td&gt;✅ Fully open&lt;/td&gt;
&lt;td&gt;❌ Closed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visuals &amp;amp; extras&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ heatmap&lt;/td&gt;
&lt;td&gt;✅ add-ons available&lt;/td&gt;
&lt;td&gt;✅ built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Analytics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ SQL-like queries, filtered views&lt;/td&gt;
&lt;td&gt;✅ Free, add-ons&lt;/td&gt;
&lt;td&gt;⚠️ Basic free, advanced paid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  How FSRS understands your memory
&lt;/h2&gt;

&lt;p&gt;FSRS calculates three parameters for each card based on review history (date and rating):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;How it changes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Difficulty&lt;/strong&gt; (D)&lt;/td&gt;
&lt;td&gt;How hard the material is&lt;/td&gt;
&lt;td&gt;Stays nearly constant — a hard topic stays hard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Stability&lt;/strong&gt; (S)&lt;/td&gt;
&lt;td&gt;How firmly the memory is held, in days&lt;/td&gt;
&lt;td&gt;Grows with each successful review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Retrievability&lt;/strong&gt; (R)&lt;/td&gt;
&lt;td&gt;Probability of recall right now&lt;/td&gt;
&lt;td&gt;Falls every second after review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After each answer (&lt;strong&gt;Again / Hard / Good / Easy&lt;/strong&gt;) the algorithm recalculates difficulty and stability. Retrievability decays on its own — when it drops below the threshold, the card appears in the review list.&lt;/p&gt;

&lt;p&gt;The threshold is configurable: want 90% retention? You'll review more often. 80% is enough? Fewer reviews.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68yizilkq1gcc1eejnql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68yizilkq1gcc1eejnql.png" alt="DSR model diagram" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How it looks in Obsidian
&lt;/h2&gt;

&lt;p&gt;Nothing but Obsidian is required. Install the plugin and go.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add FSRS fields to a note
&lt;/h3&gt;

&lt;p&gt;Open the note you want to turn into a card. Call up the command palette (&lt;code&gt;Ctrl/Cmd+P&lt;/code&gt;) and run:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FSRS: ＋ Add FSRS fields to frontmatter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;reviews&lt;/code&gt; array will appear in the frontmatter — this is where the plugin stores review history. A review button is added right after the frontmatter:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv3emetduc46d265bey1z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv3emetduc46d265bey1z.png" alt="adding FSRS fields via command palette" width="749" height="494"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;reviews&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;fsrs-review-button
&lt;/span&gt;&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In reading view, the code block becomes a button. Click it to see four rating options: &lt;strong&gt;Again&lt;/strong&gt;, &lt;strong&gt;Hard&lt;/strong&gt;, &lt;strong&gt;Good&lt;/strong&gt;, &lt;strong&gt;Easy&lt;/strong&gt;. No need to switch to edit mode.&lt;/p&gt;

&lt;p&gt;You don't have to insert the button manually — the plugin adds it automatically when initializing a card. If you disable auto-add in settings, you can insert it via &lt;strong&gt;FSRS: □ Insert review button block&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The plugin records dates and ratings into the &lt;code&gt;reviews&lt;/code&gt; field. After a couple of reviews, the frontmatter will look something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;reviews&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2025-03-15T12:00:00Z"&lt;/span&gt;
    &lt;span class="na"&gt;rating&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2025-03-17T08:00:00Z"&lt;/span&gt;
    &lt;span class="na"&gt;rating&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rating&lt;/code&gt; value: 0 = Again, 1 = Hard, 2 = Good, 3 = Easy.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a review table
&lt;/h3&gt;

&lt;p&gt;In a separate note (e.g. your daily note), open the command palette and run:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FSRS: ⬒ Insert default fsrs-table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No SQL knowledge needed — the command inserts a ready-to-use block. It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;fsrs-table
&lt;/span&gt;&lt;span class="sb"&gt;SELECT file as " ", difficulty as "D",
       stability as "S", retrievability as "R",
       date_format(due, '%Y-%m-%d') as "Next"
LIMIT 20&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In reading view, the block becomes a table with all your cards, sorted by urgency — most forgotten first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg97kez76wo57o7lbd1g8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg97kez76wo57o7lbd1g8.png" alt="rendered fsrs-table with cards" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Review without leaving the table
&lt;/h3&gt;

&lt;p&gt;This is the main workflow. Hover over a filename in the table — a popup appears with the note content and the review buttons inside it. Rate the card and move on to the next.&lt;/p&gt;

&lt;p&gt;The entire review cycle stays in one window:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the note with the table&lt;/li&gt;
&lt;li&gt;See which cards are due&lt;/li&gt;
&lt;li&gt;Hover over a card — read the content&lt;/li&gt;
&lt;li&gt;Click a rating — the card updates&lt;/li&gt;
&lt;li&gt;Move to the next row&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Later you can customize the query. Here are some practical examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHERE retrievability &amp;lt; 0.6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cards about to be forgotten&lt;/td&gt;
&lt;td&gt;Review before it's too late&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHERE state = "New"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cards with no reviews yet&lt;/td&gt;
&lt;td&gt;See what you added today&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHERE file = "Topic"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A specific note (by filename without path or extension)&lt;/td&gt;
&lt;td&gt;Check one card's status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ORDER BY reps DESC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Most-reviewed cards&lt;/td&gt;
&lt;td&gt;See what you've practiced most&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All available fields and conditions are in the &lt;a href="https://gitlab.com/Evgene-Kopylov/FSRS-plugin/-/blob/main/docs/intended_use.en.md" rel="noopener noreferrer"&gt;user guide&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Review heatmap
&lt;/h3&gt;

&lt;p&gt;To see your progress over the year — how many reviews per day — insert the block via &lt;code&gt;FSRS: Insert heatmap fsrs-heatmap&lt;/code&gt; (Ctrl/Cmd+P) or create it manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;fsrs-heatmap
&lt;/span&gt;&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In reading view, a day-grid appears showing your activity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ji4v75e5tylymv2hwgf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ji4v75e5tylymv2hwgf.png" alt="heatmap with tooltip" width="739" height="150"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;The plugin is available in the Obsidian community catalog.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Settings → Community plugins → Browse&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;FSRS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Install&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enable the plugin in &lt;strong&gt;Settings → Community plugins&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What else
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://gitlab.com/Evgene-Kopylov/FSRS-plugin/-/blob/main/docs/intended_use.en.md" rel="noopener noreferrer"&gt;User guide&lt;/a&gt; — step-by-step instructions for all features&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/evgenekopylov/fsrs-plugin-for-obsidian-rustwasm-architecture-and-performance-3kho?preview=5a98e5cd852127f89185ad5b78ec84949da6c027f1c4afa49b02c20a13510c34368c6d1cbfbaa5b483b68465d4c66e6536b906813d9520b0d2e6538b"&gt;Technical article&lt;/a&gt; — architecture, Rust/WASM, performance&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Evgene-Kopylov/fsrs_plugin" rel="noopener noreferrer"&gt;Repository on GitHub&lt;/a&gt; — source code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Evgene Kopylov, 2026&lt;/em&gt;&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>srs</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
