<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>hi, it&#39;s mike</title>
    <link>https://mike.puddingtime.org/tags/crm/</link>
    <description>Recent content on hi, it&#39;s mike</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>mike@puddingtime.org (mike)</managingEditor>
    <webMaster>mike@puddingtime.org (mike)</webMaster>
    <copyright>© 2026, mike</copyright>
    <lastBuildDate>Thu, 13 Apr 2023 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://mike.puddingtime.org/tags/crm/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Making a plaintext personal CRM with org-contacts</title>
      <link>https://mike.puddingtime.org/posts/2023-04-13-making-a-plaintext-personal-crm-with-org-contacts/</link>
      <pubDate>Thu, 13 Apr 2023 00:00:00 +0000</pubDate><author>mike@puddingtime.org (mike)</author>
      <guid>https://mike.puddingtime.org/posts/2023-04-13-making-a-plaintext-personal-crm-with-org-contacts/</guid>
      <description>I don&amp;rsquo;t like the looks of any of the personal CRM software out there, so I&amp;rsquo;m making a plaintext one.</description>
      <content:encoded><![CDATA[<p>This morning Al and I took our coffee walk, but she had to hop on a call, so for the two-mile walk back home I had some time to think about the habit on my list that popped up today: <code>Social Maintenance</code> and I also happen to have, for assorted reasons, a massive amount of poorly directed nervous energy. I am scattered and my thoughts are darting all over the place, and there&rsquo;s enough jittery energy built up  that the thought of cycling through a bunch of &ldquo;what if I try <em>this</em>&rdquo; stuff is sort of comforting.</p>
<p>I started trying to cultivate a social maintenance habit with the thought in mind that I had no idea what I was really thinking, just that during this current period it is important to me to keep up social contact in ways large and small. Pretty soon thereafter I realized I had an organizational problem on my hands: My address books were kind of a mess. Not very well organized, old data, a ton of contacts with old work addresses, etc. I spent a day straightening that out and got to a place of mostly clean.</p>
<p>The next problem that presented itself was that &ldquo;personal CRM&rdquo; is just an  awful software category. Whenever I see a new contact management app, I think &ldquo;oh, this is surely the one that will let you do something with your existing information, or add useful information,&rdquo; but it never seems to be. The more competent looking entries in the market cost a lot. Searching yields a lot of &ldquo;make one in Trello,&rdquo; &ldquo;make one in Notion,&rdquo; etc.</p>
<p>I just stopped thinking about it and decided &ldquo;do it off the top of your head until something comes up for you.&rdquo;</p>
<p>So this morning, Al was on her call, and I had a <code>Social Maintenance</code> habit popping up on my agenda as a thing I was supposed to do today, and I&rsquo;d been reading about <code>org-contacts</code>, <code>org-vcard</code>, and the ways they can integrate with an org-mode agenda to show birthdays or other anniversaries. By a few blocks later I&rsquo;d thought of how I might wedge CRM-like data into that system with org <code>:PROPERTIES:</code> drawers:</p>
<ul>
<li>desired frequency</li>
<li>date last contacted</li>
<li>notes on the last contact</li>
</ul>
<p>This is all stuff you could do in a spreadsheet, and I think a lot of people do it that way. I have an aversion to spreadsheet applications, though.</p>
<p>By the time we were home, I had the beginnings of a plan: Export my macOS address book to a big vcard file, use <code>org-vcard</code> to import it into a contacts file, then start figuring out the mechanics of adding the fields I needed to drive org agenda views.</p>
<h2 id="getting-my-contacts-into-org-contacts">Getting my contacts into org-contacts</h2>
<p>There&rsquo;s an <code>org-vcard</code> package that theoretically handles the process of moving a vcard file into an org file. The maintainer has announced that they&rsquo;re not going to work on it any longer, and it seems to have problems with macOS Contacts output.</p>
<p>I put together a script (well, ChatGPT and I put together a script) that parses a VCF file and dumps the contacts out into the right format. I just cat&rsquo;d its output into the right file. It is probably best described as a menace to your data. I had so recently scrubbed my contacts that I trusted it enough.</p>
<p>An org-contacts record looks like this:</p>






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">\* Joe Grudd :social:
</span></span><span class="line"><span class="cl">:PROPERTIES:
</span></span><span class="line"><span class="cl">:EMAIL: joe@grudd.com
</span></span><span class="line"><span class="cl">:WEBSITE: http://joe.grudd.com
</span></span><span class="line"><span class="cl">:CONTACTED: 2023-04-12
</span></span><span class="line"><span class="cl">:END:</span></span></code></pre></div>
<p>There are a few other fields, like birthday and physical address, too.</p>
<p>On its own, it doesn&rsquo;t do a ton. You can add notes to a :NOTES: property if you like, and you can search the entire file with an <code>org-contacts</code> command that lists results instead of just doing a normal text search operation.</p>
<h2 id="tracking-contact-information">Tracking contact information</h2>
<p>There are a few ways I thought of to come at what I wanted to do, which amounted to:</p>
<ul>
<li>
<p>Keeping track of whom I&rsquo;ve had contact with</p>
</li>
<li>
<p>Keeping track of when I last had contact with someone</p>
</li>
<li>
<p>Keeping track of useful details about people</p>
</li>
<li>
<p>Surfacing people I haven&rsquo;t seen in a while</p>
<p>The <code>NOTES</code> property in a vcard record is fine, but org-mode provides a way to add a log to each record in its own drawer, which changes the record to look like this:</p>
</li>
</ul>
<!--listend-->






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-org" data-lang="org"><span class="line"><span class="cl">\* Joe Grudd :social:
</span></span><span class="line"><span class="cl"><span class="c">:PROPERTIES:
</span></span></span><span class="line"><span class="cl"><span class="cs">:EMAIL: joe@grudd.com
</span></span></span><span class="line"><span class="cl"><span class="cs">:WEBSITE: http://joe.grudd.com
</span></span></span><span class="line"><span class="cl"><span class="cs">:CONTACTED: 2023-04-12
</span></span></span><span class="line"><span class="cl"><span class="c">:END:</span>
</span></span><span class="line"><span class="cl"><span class="c">:LOGBOOK:
</span></span></span><span class="line"><span class="cl"><span class="cs">  - Note taken on [2023-04-12 Wed 11:16] \\
</span></span></span><span class="line"><span class="cl"><span class="cs">    Caught up over IM for the first time in a while. He&#39;s moving to California next month.
</span></span></span><span class="line"><span class="cl"><span class="c">  :END:</span></span></span></code></pre></div>
<p>org-mode pretties all this stuff up, so the <code>LOGBOOK</code> and <code>PROPERTIES</code> drawers aren&rsquo;t always visible.</p>
<p>I also added the <code>CONTACTED</code> field to <code>PROPERTIES</code>. It&rsquo;s just an ISO-8601 date meant to reflect the last time I had some kind of contact, even if it&rsquo;s just a ping.</p>
<p>So at this point, I could just use this as is and it&rsquo;d be no worse than a spreadsheet.</p>
<h2 id="automating-updates">Automating updates</h2>
<p>I wanted a way to quickly note a contact &ldquo;touch&rdquo; so I made a few functions for that:</p>






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">org-set-contacted-today</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Set the CONTACTED property of the current item to today&#39;s date.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="nb">interactive</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="nv">org-set-property</span> <span class="s">&#34;CONTACTED&#34;</span> <span class="p">(</span><span class="nf">format-time-string</span> <span class="s">&#34;%Y-%m-%d&#34;</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">org-set-contacted-date</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Set the CONTACTED property of the current item to a chosen date.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="nb">interactive</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="nb">let</span> <span class="p">((</span><span class="nv">date</span> <span class="p">(</span><span class="nv">org-read-date</span> <span class="no">nil</span> <span class="no">t</span> <span class="no">nil</span> <span class="s">&#34;Enter the date: &#34;</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="nv">org-set-property</span> <span class="s">&#34;CONTACTED&#34;</span> <span class="p">(</span><span class="nf">format-time-string</span> <span class="s">&#34;%Y-%m-%d&#34;</span> <span class="nv">date</span><span class="p">))))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nv">map!</span> <span class="nb">:mode</span> <span class="nv">org-mode</span>
</span></span><span class="line"><span class="cl">      <span class="nb">:localleader</span>
</span></span><span class="line"><span class="cl">      <span class="nb">:desc</span> <span class="s">&#34;Set CONTACTED property to today&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;c t&#34;</span> <span class="nf">#&#39;</span><span class="nv">org-set-contacted-today</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;c d&#34;</span> <span class="nf">#&#39;</span><span class="nv">org-set-contacted-date</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;c z&#34;</span> <span class="nf">#&#39;</span><span class="nv">my/org-remove-todo</span>
</span></span><span class="line"><span class="cl">                <span class="p">)</span></span></span></code></pre></div>
<p>Those two allow me to set the <code>CONTACTED</code> property either to today&rsquo;s date (<code>spc m c t</code>), or by interactively selecting a date (<code>spc m c d</code>). There&rsquo;s a third mapping that lets me z out the TODO status of a contact (<code>spc m c z</code>), which I will get to.</p>
<h2 id="agenda-customization">Agenda customization</h2>
<p>Next up, I wanted some kind of agenda automation &ndash; custom views that&rsquo;d let me see contacts overdue for some kind of ping. I made a few driven by a combination of tags and age of the <code>CONTACTED</code> field.</p>
<p>Here&rsquo;s one of them, driven by a function that finds aged contacts:</p>






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nv">add-to-list</span> <span class="ss">&#39;org-agenda-custom-commands</span>
</span></span><span class="line"><span class="cl">             <span class="o">&#39;</span><span class="p">(</span><span class="s">&#34;N&#34;</span> <span class="s">&#34;Professional network last contacted &gt; 90 days ago&#34;</span>
</span></span><span class="line"><span class="cl">               <span class="p">((</span><span class="nv">tags</span> <span class="s">&#34;network&#34;</span>
</span></span><span class="line"><span class="cl">                      <span class="p">((</span><span class="nv">org-agenda-overriding-header</span> <span class="s">&#34;Network contacts, not contacted in the past 90 days&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                       <span class="p">(</span><span class="nv">org-tags-match-list-sublevels</span> <span class="no">t</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                       <span class="p">(</span><span class="nv">org-agenda-skip-function</span>
</span></span><span class="line"><span class="cl">                        <span class="p">(</span><span class="nb">lambda</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl">                          <span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nv">org-contacted-more-than-days-ago</span> <span class="mi">90</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                            <span class="p">(</span><span class="nb">or</span> <span class="p">(</span><span class="nv">outline-next-heading</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                                <span class="p">(</span><span class="nf">goto-char</span> <span class="p">(</span><span class="nf">point-max</span><span class="p">))))))))</span>
</span></span><span class="line"><span class="cl">                <span class="p">)))</span></span></span></code></pre></div>
<p>So in Doom, I can tap <code>spc oAN</code> and get a list of contacts tagged with <code>network</code> whom I haven&rsquo;t had any contact with for more than 90 days.</p>
<h2 id="setting-priority-by-touch-date">Setting priority by touch date</h2>
<p>I wanted a way to see which contacts were aging and decided to use plain old priorities for that, so a function looks at the difference between today and <code>CONTACTED</code> and prioritizes more aged contacts higher:</p>






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">my/org-update-priorities-based-on-contacted</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="nb">interactive</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="nb">save-excursion</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="nf">goto-char</span> <span class="p">(</span><span class="nf">point-min</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="nb">while</span> <span class="p">(</span><span class="nf">re-search-forward</span> <span class="s">&#34;^\\*+ &#34;</span> <span class="no">nil</span> <span class="no">t</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">(</span><span class="nb">let</span> <span class="p">((</span><span class="nv">contacted-date</span> <span class="p">(</span><span class="nv">org-entry-get</span> <span class="p">(</span><span class="nf">point</span><span class="p">)</span> <span class="s">&#34;CONTACTED&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="p">(</span><span class="nv">today</span> <span class="p">(</span><span class="nf">format-time-string</span> <span class="s">&#34;%Y-%m-%d&#34;</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="nb">when</span> <span class="nv">contacted-date</span>
</span></span><span class="line"><span class="cl">          <span class="p">(</span><span class="nb">let</span> <span class="p">((</span><span class="nv">days-ago</span> <span class="p">(</span><span class="nf">-</span> <span class="p">(</span><span class="nv">time-to-days</span> <span class="p">(</span><span class="nf">current-time</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">                             <span class="p">(</span><span class="nv">time-to-days</span> <span class="p">(</span><span class="nv">org-time-string-to-time</span> <span class="nv">contacted-date</span><span class="p">)))))</span> <span class="c1">; calculate days since CONTACTED date</span>
</span></span><span class="line"><span class="cl">            <span class="p">(</span><span class="nv">org-set-property</span> <span class="s">&#34;PRIORITY&#34;</span>
</span></span><span class="line"><span class="cl">                               <span class="p">(</span><span class="nb">cond</span>
</span></span><span class="line"><span class="cl">                                <span class="p">((</span><span class="nf">&lt;</span> <span class="nv">days-ago</span> <span class="mi">45</span><span class="p">)</span> <span class="s">&#34;C&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                                <span class="p">((</span><span class="nf">&lt;</span> <span class="nv">days-ago</span> <span class="mi">90</span><span class="p">)</span> <span class="s">&#34;B&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                                <span class="p">(</span><span class="no">t</span> <span class="s">&#34;A&#34;</span><span class="p">)))))))))</span></span></span></code></pre></div>
<p>It&rsquo;s connected to a save hook, so every contact&rsquo;s priority gets recalculated at save:</p>






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="nv">add-hook</span> <span class="ss">&#39;after-save-hook</span>
</span></span><span class="line"><span class="cl">          <span class="p">(</span><span class="nb">lambda</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nf">string-equal</span> <span class="p">(</span><span class="nf">buffer-file-name</span><span class="p">)</span> <span class="s">&#34;~/org/contacts.org&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">              <span class="p">(</span><span class="nv">my/org-update-priorities-based-on-contacted</span> <span class="p">))))</span></span></span></code></pre></div>
<p>That prioritization shows up in the agenda views I set up, with <code>[A]</code> priority contacts getting their own section.</p>
<h2 id="custom-todo-states">Custom TODO states</h2>
<p>Finally, I wanted a way to keep track of where a given contact is, or cue myself on next steps, so I set up custom TODO states just for my <code>contacts.org</code> file:</p>
<p><code>#+TODO: PING(p) PINGED(P!) FOLLOWUP(f) SKED(s) | TIMEOUT(t) OK(o)</code></p>
<p>By putting a <code>!</code> inside the shortcut parens, org-mode will automatically log changes in and out of those states. For now I just have <code>PINGED</code> wired up that way, and <code>TIMEOUT</code> and <code>OK</code>, as <code>DONE</code> equivalents will similarly trigger a log entry.</p>
<p><code>FOLLOWUP</code> and <code>SKED</code> are there as reminders that I need to do something next. <code>TIMEOUT</code> is a way to tell myself I gave it a shot and nothing came of it. <code>OK</code> is just an interim state on the way to no state until the agenda surfaces someone again.</p>
<p>The logging for these state changes looks like this in a given contact entry:</p>






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-org" data-lang="org"><span class="line"><span class="cl">\* PINGED Joe Grudd :social:
</span></span><span class="line"><span class="cl"><span class="c">:PROPERTIES:
</span></span></span><span class="line"><span class="cl"><span class="cs">:EMAIL: joe@grudd.com
</span></span></span><span class="line"><span class="cl"><span class="cs">:WEBSITE: http://joe.grudd.com
</span></span></span><span class="line"><span class="cl"><span class="cs">:CONTACTED: 2023-04-12
</span></span></span><span class="line"><span class="cl"><span class="c">:END:</span>
</span></span><span class="line"><span class="cl"><span class="c">:LOGBOOK:
</span></span></span><span class="line"><span class="cl"><span class="cs">- State &#34;PINGED&#34;     from &#34;PING&#34;   [2023-04-11 Tue 20:21]
</span></span></span><span class="line"><span class="cl"><span class="cs">- Note taken on [2023-04-12 Wed 11:16] \\
</span></span></span><span class="line"><span class="cl"><span class="cs">  Caught up over text for the first time in a while. He&#39;s moving to California next month.
</span></span></span><span class="line"><span class="cl"><span class="c">:END:</span></span></span></code></pre></div>
<p>This is also where that last &ldquo;z&rdquo; keybinding comes in:</p>






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">defun</span> <span class="nv">my/org-remove-todo</span> <span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="nb">interactive</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="nv">org-set-property</span> <span class="s">&#34;TODO&#34;</span> <span class="s">&#34;&#34;</span><span class="p">))</span></span></span></code></pre></div>
<p>With <code>spc m c z</code> I can zero out the TODO state of a given contact without triggering a log entry, keeping a little bit of noise down.</p>
<h2 id="what-else">What else?</h2>
<p>That&rsquo;s the system. I guess the summary is:</p>
<p>&ldquo;I&rsquo;ve used org-contacts to keep my contacts in a plaintext file. By using existing data features and agenda customizations, I get prompts that will help me cultivate my <code>Social Maintenance</code> habit when it&rsquo;s due. With a few custom functions and a save hook, I can use light automation to make the text more dynamic without a lot of day-to-day effort.&rdquo;</p>
<p>And I&rsquo;m a little more clear on what &ldquo;social maintenance&rdquo; can mean, now. I think I&rsquo;d created an ill-defined monster the longer I let it sit there with no shape. As I put this together I got to think about what would be meaningful, and I realized it just makes my day when I get a text from someone asking how it&rsquo;s going, so that&rsquo;s a fine standard to apply.</p>
<p>And yes, it was an interesting ChatGPT exercise. It would have taken me days to suss all this out on my own. I just don&rsquo;t have the elisp. It took much less time just dialoging with the bot, and it let me work much more iteratively if an idea didn&rsquo;t test quite right. I wonder how this would have gone if I&rsquo;d thought of trying it when I was using Obsidian a lot, or if I&rsquo;d been in more of a Rails or Sinatra mood.</p>
<p>I think the whole thing will seem like overkill to some, but I am not good at keeping up with people. I am not going to go all autobiographical to explain it, I&rsquo;m just gonna say that there is what I want to do and there is what I do, and they aren&rsquo;t aligned, and I know enough about myself to know that in the absence of a supporting system my good intentions will not mean anything.</p>
<p>And I&rsquo;ve had a few recent interactions with people I haven&rsquo;t spoken to in a long time. It feels really good to reconnect, even if it&rsquo;s just a few lines of &ldquo;what&rsquo;s up with you?&rdquo; So I&rsquo;ve built a system to help me get more of that.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
