<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by David Klein on Medium]]></title>
        <description><![CDATA[Stories by David Klein on Medium]]></description>
        <link>https://medium.com/@david.klein_4417?source=rss-3841e0d4c4fb------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*Wv3BokLlYqgJKxFkodyF-w.jpeg</url>
            <title>Stories by David Klein on Medium</title>
            <link>https://medium.com/@david.klein_4417?source=rss-3841e0d4c4fb------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 23 May 2026 12:24:46 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@david.klein_4417/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How I Connected Strava to ChatGPT to Get Real Post-Workout Analysis]]></title>
            <link>https://medium.com/@david.klein_4417/how-i-connected-strava-to-chatgpt-to-get-real-post-workout-analysis-88084cc4c48f?source=rss-3841e0d4c4fb------2</link>
            <guid isPermaLink="false">https://medium.com/p/88084cc4c48f</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[chatgpt]]></category>
            <category><![CDATA[strava]]></category>
            <category><![CDATA[cycling]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[David Klein]]></dc:creator>
            <pubDate>Tue, 02 Dec 2025 08:00:50 GMT</pubDate>
            <atom:updated>2025-12-02T08:00:50.364Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pT_ULIqJbJOgNSPfWvG2ZQ.png" /></figure><p><em>(For this setup ChatGPT Plus required)</em></p><p>Training apps are good at logging data, but they rarely tell you what it <em>means</em>.<br>Strava shows the ride. TrainingPeaks shows the numbers or long term plans. Zwift helps me hit my numbers. What I always missed was something simple:</p><p><strong>“Tell me what actually happened in my workout, like a coach would.”</strong></p><p>I used to work with a real trainer in the past, but with my limited time budget, family, work and travel, there is too much effort involved from both sides to make this really work, though I still don’t think you can really substitute a trained human coach, if you have the chance to work with one.</p><p>When OpenAI released custom GPTs with API connections, I realized I could streamline this part</p><p>This article explains how I built a <strong>post-workout analysis GPT</strong>, how it connects to Strava, and what you <em>should</em> and <em>shouldn’t</em> expect from it.</p><p>This is not a training plan generator.<br>It’s not a replacement for a long-term coach.<br>It’s a tool for <strong>interpreting single workouts extremely well</strong>.</p><h3>Why I Wanted a Post-Workout GPT</h3><p>After every ride I found myself asking:</p><ul><li>Was my cadence stable or fading?</li><li>Did I pace intervals correctly?</li><li>Was there HR drift?</li><li>Was this SST session actually SST or something else?</li><li>Did fatigue show up?</li></ul><p>The insights you get from strava are nice to have, but honestly, they don’t tell me much at least don’t answer these questions for me sufficiently.<br>As I said before: human coaches do, but they cost money and don’t check every single workout.</p><p>What I wanted was:</p><ul><li><strong>an immediate breakdown after each ride</strong>,</li><li><strong>based on my Strava data: no screenshots,</strong></li><li><strong>with the logic of a trained endurance coach</strong>,</li><li><strong>without trying to plan my whole season</strong>.</li></ul><p>And that’s exactly what a custom GPT can do well.</p><h3>The Limitations You Should Know Up Front</h3><p>A GPT is <strong>not</strong> a season planner.<br>It has no long-term memory, cannot remember previous days, and cannot build continuity in the way a dedicated training platform or real coach can.</p><p>Yes, you <em>could</em> upload PDFs with traning philosophy, rider history, or block structures, but this is only static reference material. GPTs do not continuously remember your actual training unless you paste it into the chat every time.</p><p>That’s why I decided to split responsibilities:</p><ul><li><strong>Standard ChatGPT Chat → Spreadsheets / external notes → long-term planning</strong></li><li><strong>Custom GPT → per-workout interpretation</strong></li></ul><p>This keeps expectations realistic and the tool genuinely helpful (might write about long-term planning with a standard chat in the future, but honestly still haven’t fully figured out this part myself.)</p><h3>Allowing the Strava API to Talk to ChatGPT</h3><p>Before building any training logic or analysis prompts, the first thing you need is the Strava data itself. Fortunately, it’s now possible to let a custom GPT pull your activities directly from the Strava API — <strong>but only if you’re a ChatGPT Plus subscriber</strong>, because custom GPTs and Actions are part of the Plus tier.</p><p>Here’s a simple, reliable walkthrough of how to connect Strava to ChatGPT so your workouts can be analyzed automatically.</p><h3>In Strava Webinterface: 1. Create a Strava API Application</h3><p>In Strava, go to:</p><p><strong>Settings → My API Application → Create App</strong></p><p>Fill in the basic fields (name, website, etc.), and make sure you set:</p><p><strong>Authorization Callback Domain:</strong></p><pre>chat.openai.com</pre><p>This is required so Strava can send the OAuth approval back into ChatGPT.</p><p>Once the app is created, Strava provides:</p><ul><li><strong>Client ID</strong></li><li><strong>Client Secret</strong></li></ul><p>Keep those ready for later.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xuQgfFmylB1330b639UxnA.jpeg" /><figcaption>This is what it should look like, in my case German (sorry for that)</figcaption></figure><h3>In ChatGPT: 2. Create a Custom GPT in ChatGPT</h3><p>Inside ChatGPT:</p><p><strong>GPTs → + Create</strong></p><p>Give it a name and a short description.<br>You can refine the instructions later, the important part right now is the Strava connection.</p><h3>3. Add a Strava Action</h3><p>Scroll down in the GPT builder until you reach:</p><p><strong>Actions → Create new action</strong></p><p>This is where you configure the interface between ChatGPT and Strava.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/764/1*hfObO15VuzvbY8OSpwQAnQ.png" /><figcaption>After you create GPT, create the Action here</figcaption></figure><h3>4. Configure OAuth (the authentication layer)</h3><p>In the action settings:</p><ul><li>Change <strong>Authentication: None → OAuth</strong></li><li>Insert your <strong>Client ID</strong> and <strong>Client Secret</strong> from Strava</li><li>Then fill in these required fields:</li></ul><p><strong>Authorization URL:</strong></p><pre>https://www.strava.com/oauth/authorize</pre><p><strong>Token URL:</strong></p><pre>https://www.strava.com/api/v3/oauth/token</pre><p><strong>Scope:</strong></p><pre>read,read_all,activity:read,profile:read_all</pre><p>This scope gives the GPT enough permission to:</p><ul><li>read your activities</li><li>read your heart-rate, power, cadence, distance, etc.</li><li>read your profile and zones</li></ul><p>Once done, click <strong>Save OAuth configuration</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/493/1*o7qNjWN9Fe5PBbAKnjhfOg.png" /><figcaption>In the end it should similar to this</figcaption></figure><h3>5. Add the Strava API Schema</h3><p>Now scroll down to the <strong>Schema</strong> field.</p><p>This tells ChatGPT which API endpoints it may call.<br>You don’t need the full Strava API, just a minimal, stable subset, but you don’t need to care, just copy and paste this into the schema part:</p><pre>{<br>  &quot;openapi&quot;: &quot;3.1.0&quot;,<br>  &quot;info&quot;: {<br>    &quot;title&quot;: &quot;Strava API&quot;,<br>    &quot;description&quot;: &quot;OpenAPI specification for accessing Strava fitness data via OAuth2.&quot;,<br>    &quot;version&quot;: &quot;v1.0.0&quot;<br>  },<br>  &quot;servers&quot;: [<br>    {<br>      &quot;url&quot;: &quot;https://www.strava.com/api/v3&quot;<br>    }<br>  ],<br>  &quot;paths&quot;: {<br>    &quot;/athlete&quot;: {<br>      &quot;get&quot;: {<br>        &quot;operationId&quot;: &quot;getAuthenticatedAthlete&quot;,<br>        &quot;summary&quot;: &quot;Get the currently authenticated athlete&quot;,<br>        &quot;security&quot;: [<br>          {<br>            &quot;stravaOAuth&quot;: [<br>              &quot;profile:read_all&quot;<br>            ]<br>          }<br>        ],<br>        &quot;responses&quot;: {<br>          &quot;200&quot;: {<br>            &quot;description&quot;: &quot;Returns the authenticated athlete&quot;,<br>            &quot;content&quot;: {<br>              &quot;application/json&quot;: {<br>                &quot;schema&quot;: {<br>                  &quot;$ref&quot;: &quot;#/components/schemas/Athlete&quot;<br>                }<br>              }<br>            }<br>          }<br>        }<br>      }<br>    },<br>    &quot;/athlete/zones&quot;: {<br>      &quot;get&quot;: {<br>        &quot;operationId&quot;: &quot;getAthleteZones&quot;,<br>        &quot;summary&quot;: &quot;Get athlete heart rate and power zones&quot;,<br>        &quot;security&quot;: [<br>          {<br>            &quot;stravaOAuth&quot;: [<br>              &quot;profile:read_all&quot;<br>            ]<br>          }<br>        ],<br>        &quot;responses&quot;: {<br>          &quot;200&quot;: {<br>            &quot;description&quot;: &quot;Athlete zones data&quot;,<br>            &quot;content&quot;: {<br>              &quot;application/json&quot;: {<br>                &quot;schema&quot;: {<br>                  &quot;type&quot;: &quot;object&quot;,<br>                  &quot;properties&quot;: {}<br>                }<br>              }<br>            }<br>          }<br>        }<br>      }<br>    },<br>    &quot;/athletes/{id}/stats&quot;: {<br>      &quot;get&quot;: {<br>        &quot;operationId&quot;: &quot;getAthleteStats&quot;,<br>        &quot;summary&quot;: &quot;Get aggregate athlete stats&quot;,<br>        &quot;security&quot;: [<br>          {<br>            &quot;stravaOAuth&quot;: [<br>              &quot;profile:read_all&quot;<br>            ]<br>          }<br>        ],<br>        &quot;parameters&quot;: [<br>          {<br>            &quot;name&quot;: &quot;id&quot;,<br>            &quot;in&quot;: &quot;path&quot;,<br>            &quot;required&quot;: true,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;integer&quot;<br>            }<br>          }<br>        ],<br>        &quot;responses&quot;: {<br>          &quot;200&quot;: {<br>            &quot;description&quot;: &quot;Aggregate stats&quot;,<br>            &quot;content&quot;: {<br>              &quot;application/json&quot;: {<br>                &quot;schema&quot;: {<br>                  &quot;type&quot;: &quot;object&quot;,<br>                  &quot;properties&quot;: {}<br>                }<br>              }<br>            }<br>          }<br>        }<br>      }<br>    },<br>    &quot;/athlete/activities&quot;: {<br>      &quot;get&quot;: {<br>        &quot;operationId&quot;: &quot;getAthleteActivities&quot;,<br>        &quot;summary&quot;: &quot;List activities of the authenticated athlete&quot;,<br>        &quot;security&quot;: [<br>          {<br>            &quot;stravaOAuth&quot;: [<br>              &quot;activity:read_all&quot;<br>            ]<br>          }<br>        ],<br>        &quot;parameters&quot;: [<br>          {<br>            &quot;name&quot;: &quot;before&quot;,<br>            &quot;in&quot;: &quot;query&quot;,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;integer&quot;<br>            }<br>          },<br>          {<br>            &quot;name&quot;: &quot;after&quot;,<br>            &quot;in&quot;: &quot;query&quot;,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;integer&quot;<br>            }<br>          },<br>          {<br>            &quot;name&quot;: &quot;page&quot;,<br>            &quot;in&quot;: &quot;query&quot;,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;integer&quot;<br>            }<br>          },<br>          {<br>            &quot;name&quot;: &quot;per_page&quot;,<br>            &quot;in&quot;: &quot;query&quot;,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;integer&quot;<br>            }<br>          }<br>        ],<br>        &quot;responses&quot;: {<br>          &quot;200&quot;: {<br>            &quot;description&quot;: &quot;List of activities&quot;,<br>            &quot;content&quot;: {<br>              &quot;application/json&quot;: {<br>                &quot;schema&quot;: {<br>                  &quot;type&quot;: &quot;array&quot;,<br>                  &quot;items&quot;: {<br>                    &quot;$ref&quot;: &quot;#/components/schemas/ActivitySummary&quot;<br>                  }<br>                }<br>              }<br>            }<br>          }<br>        }<br>      }<br>    },<br>    &quot;/activities/{id}&quot;: {<br>      &quot;get&quot;: {<br>        &quot;operationId&quot;: &quot;getActivityById&quot;,<br>        &quot;summary&quot;: &quot;Get activity details&quot;,<br>        &quot;security&quot;: [<br>          {<br>            &quot;stravaOAuth&quot;: [<br>              &quot;activity:read_all&quot;<br>            ]<br>          }<br>        ],<br>        &quot;parameters&quot;: [<br>          {<br>            &quot;name&quot;: &quot;id&quot;,<br>            &quot;in&quot;: &quot;path&quot;,<br>            &quot;required&quot;: true,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;integer&quot;<br>            }<br>          },<br>          {<br>            &quot;name&quot;: &quot;include_all_efforts&quot;,<br>            &quot;in&quot;: &quot;query&quot;,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;boolean&quot;<br>            }<br>          }<br>        ],<br>        &quot;responses&quot;: {<br>          &quot;200&quot;: {<br>            &quot;description&quot;: &quot;Activity details&quot;,<br>            &quot;content&quot;: {<br>              &quot;application/json&quot;: {<br>                &quot;schema&quot;: {<br>                  &quot;$ref&quot;: &quot;#/components/schemas/ActivityDetail&quot;<br>                }<br>              }<br>            }<br>          }<br>        }<br>      }<br>    },<br>    &quot;/activities/{id}/streams&quot;: {<br>      &quot;get&quot;: {<br>        &quot;operationId&quot;: &quot;getActivityStreams&quot;,<br>        &quot;summary&quot;: &quot;Get time-series streams (HR, Power, Cadence, GPS)&quot;,<br>        &quot;security&quot;: [<br>          {<br>            &quot;stravaOAuth&quot;: [<br>              &quot;activity:read_all&quot;<br>            ]<br>          }<br>        ],<br>        &quot;parameters&quot;: [<br>          {<br>            &quot;name&quot;: &quot;id&quot;,<br>            &quot;in&quot;: &quot;path&quot;,<br>            &quot;required&quot;: true,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;integer&quot;<br>            }<br>          },<br>          {<br>            &quot;name&quot;: &quot;keys&quot;,<br>            &quot;in&quot;: &quot;query&quot;,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;string&quot;<br>            }<br>          },<br>          {<br>            &quot;name&quot;: &quot;key_by_type&quot;,<br>            &quot;in&quot;: &quot;query&quot;,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;boolean&quot;<br>            }<br>          }<br>        ],<br>        &quot;responses&quot;: {<br>          &quot;200&quot;: {<br>            &quot;description&quot;: &quot;Activity streams&quot;,<br>            &quot;content&quot;: {<br>              &quot;application/json&quot;: {<br>                &quot;schema&quot;: {<br>                  &quot;type&quot;: &quot;object&quot;,<br>                  &quot;properties&quot;: {},<br>                  &quot;additionalProperties&quot;: {<br>                    &quot;type&quot;: &quot;object&quot;,<br>                    &quot;properties&quot;: {<br>                      &quot;original_size&quot;: {<br>                        &quot;type&quot;: &quot;integer&quot;<br>                      },<br>                      &quot;resolution&quot;: {<br>                        &quot;type&quot;: &quot;string&quot;<br>                      },<br>                      &quot;series_type&quot;: {<br>                        &quot;type&quot;: &quot;string&quot;<br>                      },<br>                      &quot;data&quot;: {<br>                        &quot;type&quot;: &quot;array&quot;,<br>                        &quot;items&quot;: {<br>                          &quot;type&quot;: &quot;number&quot;<br>                        }<br>                      }<br>                    }<br>                  }<br>                }<br>              }<br>            }<br>          }<br>        }<br>      }<br>    },<br>    &quot;/activities/{id}/zones&quot;: {<br>      &quot;get&quot;: {<br>        &quot;operationId&quot;: &quot;getActivityZones&quot;,<br>        &quot;summary&quot;: &quot;Get activity zone distribution&quot;,<br>        &quot;security&quot;: [<br>          {<br>            &quot;stravaOAuth&quot;: [<br>              &quot;activity:read_all&quot;<br>            ]<br>          }<br>        ],<br>        &quot;parameters&quot;: [<br>          {<br>            &quot;name&quot;: &quot;id&quot;,<br>            &quot;in&quot;: &quot;path&quot;,<br>            &quot;required&quot;: true,<br>            &quot;schema&quot;: {<br>              &quot;type&quot;: &quot;integer&quot;<br>            }<br>          }<br>        ],<br>        &quot;responses&quot;: {<br>          &quot;200&quot;: {<br>            &quot;description&quot;: &quot;Zone distribution&quot;,<br>            &quot;content&quot;: {<br>              &quot;application/json&quot;: {<br>                &quot;schema&quot;: {<br>                  &quot;type&quot;: &quot;object&quot;,<br>                  &quot;properties&quot;: {}<br>                }<br>              }<br>            }<br>          }<br>        }<br>      }<br>    }<br>  },<br><br>  &quot;components&quot;: {<br>    &quot;schemas&quot;: {<br>      &quot;Athlete&quot;: {<br>        &quot;type&quot;: &quot;object&quot;,<br>        &quot;properties&quot;: {<br>          &quot;id&quot;: {<br>            &quot;type&quot;: &quot;integer&quot;<br>          },<br>          &quot;username&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;firstname&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;lastname&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;city&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;country&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;sex&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;created_at&quot;: {<br>            &quot;type&quot;: &quot;string&quot;,<br>            &quot;format&quot;: &quot;date-time&quot;<br>          }<br>        }<br>      },<br>      &quot;ActivitySummary&quot;: {<br>        &quot;type&quot;: &quot;object&quot;,<br>        &quot;properties&quot;: {<br>          &quot;id&quot;: {<br>            &quot;type&quot;: &quot;integer&quot;<br>          },<br>          &quot;name&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;distance&quot;: {<br>            &quot;type&quot;: &quot;number&quot;<br>          },<br>          &quot;moving_time&quot;: {<br>            &quot;type&quot;: &quot;integer&quot;<br>          },<br>          &quot;elapsed_time&quot;: {<br>            &quot;type&quot;: &quot;integer&quot;<br>          },<br>          &quot;type&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;sport_type&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;start_date&quot;: {<br>            &quot;type&quot;: &quot;string&quot;,<br>            &quot;format&quot;: &quot;date-time&quot;<br>          },<br>          &quot;visibility&quot;: {<br>            &quot;type&quot;: &quot;string&quot;,<br>            &quot;enum&quot;: [<br>              &quot;everyone&quot;,<br>              &quot;followers_only&quot;,<br>              &quot;only_me&quot;<br>            ]<br>          }<br>        }<br>      },<br>      &quot;ActivityDetail&quot;: {<br>        &quot;type&quot;: &quot;object&quot;,<br>        &quot;properties&quot;: {<br>          &quot;id&quot;: {<br>            &quot;type&quot;: &quot;integer&quot;<br>          },<br>          &quot;name&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;description&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;distance&quot;: {<br>            &quot;type&quot;: &quot;number&quot;<br>          },<br>          &quot;moving_time&quot;: {<br>            &quot;type&quot;: &quot;integer&quot;<br>          },<br>          &quot;elapsed_time&quot;: {<br>            &quot;type&quot;: &quot;integer&quot;<br>          },<br>          &quot;total_elevation_gain&quot;: {<br>            &quot;type&quot;: &quot;number&quot;<br>          },<br>          &quot;type&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;sport_type&quot;: {<br>            &quot;type&quot;: &quot;string&quot;<br>          },<br>          &quot;start_date&quot;: {<br>            &quot;type&quot;: &quot;string&quot;,<br>            &quot;format&quot;: &quot;date-time&quot;<br>          },<br>          &quot;visibility&quot;: {<br>            &quot;type&quot;: &quot;string&quot;,<br>            &quot;enum&quot;: [<br>              &quot;everyone&quot;,<br>              &quot;followers_only&quot;,<br>              &quot;only_me&quot;<br>            ]<br>          },<br>          &quot;device_watts&quot;: {<br>            &quot;type&quot;: &quot;boolean&quot;<br>          },<br>          &quot;average_watts&quot;: {<br>            &quot;type&quot;: &quot;number&quot;<br>          },<br>          &quot;weighted_average_watts&quot;: {<br>            &quot;type&quot;: &quot;number&quot;<br>          },<br>          &quot;average_heartrate&quot;: {<br>            &quot;type&quot;: &quot;number&quot;<br>          },<br>          &quot;max_heartrate&quot;: {<br>            &quot;type&quot;: &quot;number&quot;<br>          }<br>        }<br>      }<br>    },<br>    &quot;securitySchemes&quot;: {<br>      &quot;stravaOAuth&quot;: {<br>        &quot;type&quot;: &quot;oauth2&quot;,<br>        &quot;flows&quot;: {<br>          &quot;authorizationCode&quot;: {<br>            &quot;authorizationUrl&quot;: &quot;https://www.strava.com/oauth/authorize&quot;,<br>            &quot;tokenUrl&quot;: &quot;https://www.strava.com/oauth/token&quot;,<br>            &quot;scopes&quot;: {<br>              &quot;read&quot;: &quot;Read public segments and routes&quot;,<br>              &quot;read_all&quot;: &quot;Read all segments and routes&quot;,<br>              &quot;profile:read_all&quot;: &quot;Read full athlete profile&quot;,<br>              &quot;activity:read&quot;: &quot;Read public activities&quot;,<br>              &quot;activity:read_all&quot;: &quot;Read all activities, including private&quot;,<br>              &quot;activity:write&quot;: &quot;Write activities&quot;<br>            }<br>          }<br>        }<br>      }<br>    }<br>  },<br><br>  &quot;security&quot;: [<br>    {<br>      &quot;stravaOAuth&quot;: [<br>        &quot;activity:read_all&quot;,<br>        &quot;profile:read_all&quot;<br>      ]<br>    }<br>  ]<br>}</pre><p>Click <strong>Update</strong>.</p><p>If everything is correct, you’ll see no errors and your GPT can now pull activities from your Strava account.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OdYF9Dtkd3ALyqLLs-ApWA.png" /><figcaption>If everything is filled in correctly there should be no errors and the Schema will allow certain actions to be taken</figcaption></figure><h3>6. Add Your Custom Instructions (Important!)</h3><p>This is where you give the GPT its “brain.”</p><p>Scroll back up to the <strong>Instructions</strong> section of your GPT. Here you define <strong>how the GPT should use the Strava data</strong>, e.g.:</p><ul><li>what type of coach it is</li><li>what exactly you are interested in (HR Drift, Cardiac Lag, Heart Rate Recovery (HRR), Neuromuscular Fatigue, Power Decoupling…)</li><li>how it should interpret the activity streams</li><li>how deep the analysis should be</li><li>whether it should pull only certain sports</li><li>how to structure its output</li><li>what context about you (the athlete) should guide its decisions</li></ul><p>This is the part where you tell it:</p><ul><li><em>“If I say ‘judge my latest workout’, fetch my newest ride and analyze it deeply.”</em></li><li><em>“Always pull power, HR, cadence streams.”</em></li><li><em>“Use my private notes in Strava to interpret fatigue.”</em></li><li><em>“Give me coaching-style feedback, not summaries.”</em></li></ul><p>These instructions are the core logic of the GPT , the API schema only lets it fetch data, the instructions tell it what to <strong>do</strong> with the data.</p><p>You can change and refine this text anytime.</p><blockquote><strong>My tip:</strong> I use private notes to tell it about the RPE, fatique, sleep quality, nutrition etc. so GPT gets the context of the session.</blockquote><p><strong>The Instructions I use personally (feel free to change and comment here what works for you). I just used ChatGPT to create this one.</strong></p><pre>You are a cycling coach.<br>Your ONLY job is to analyze single cycling workouts from Strava in depth.<br><br>When the user says things like:<br>- “judge my latest workout”<br>- “analyze my last ride”<br>- “deep dive on yesterday’s Zwift session”<br>- “go deeper on my endurance ride”<br>you MUST produce a full detailed analysis. Never keep it short unless the user explicitly asks for short.<br><br>------------------------------------------------------------<br>1. DATA ACCESS RULES<br>------------------------------------------------------------<br><br>Always fetch recent activities:<br>GET /athlete/activities?per_page=3<br><br>Activity selection logic:<br>- Use only activities where sport_type is one of:<br>  [&quot;Ride&quot;, &quot;VirtualRide&quot;, &quot;GravelRide&quot;, &quot;RoadRide&quot;, &quot;EBikeRide&quot;].<br>- If the user mentions a day (“yesterday”, “Friday”), pick the matching one.<br>- If the user mentions a name fragment (“Tempus Fugit”, “UNBOUND”), pick that one.<br>- Otherwise, pick the most recent cycling activity.<br>Never ask “public or private?” or “which ride?” if you can infer it.<br><br>Get full details:<br>GET /activities/{id}<br><br>Get streams:<br>GET /activities/{id}/streams<br>keys = &quot;time,watts,heartrate,cadence,distance&quot;<br>key_by_type = true<br><br>If stream call fails, state that only summary data is available and do a structured analysis anyway.<br><br>------------------------------------------------------------<br>2. ANALYSIS LOGIC<br>------------------------------------------------------------<br><br>Using activity details + streams, you MUST evaluate:<br><br>• Workout type:<br>  recovery, Z2 endurance, tempo, sweet spot, threshold, VO2max, long ride, race-like, mixed.<br><br>• Power:<br>  pacing consistency, variability index, interval identification, fade/no fade, execution vs intended zone.<br><br>• Heart rate:<br>  HR drift (first vs second half), interpretation of drift in context, HR vs power alignment (suppressed/elevated/normal).<br><br>• Cadence:<br>  average, stability, fatigue signals (falling cadence late), neuromuscular control.<br><br>• Fatigue markers:<br>  rising HR at same power, falling power at same HR, unstable cadence, poor efficiency.<br><br>• Context:<br>  Use description AND private notes.<br>  Private notes may contain:<br>  - sickness<br>  - bad sleep<br>  - high stress<br>  - poor fueling<br>  - dehydration<br>  - illness, fatigue<br>  ALWAYS integrate private notes into the interpretation.<br>  If private notes contradict physiological data, highlight the mismatch.<br><br>------------------------------------------------------------<br>3. EXECUTIVE SUMMARY (ALWAYS FIRST)<br>------------------------------------------------------------<br><br>Always start your answer with a 4–5 line performance judgment.<br>It must evaluate the athlete, not describe the workout.<br><br>The Executive Summary MUST clearly state:<br>- whether execution was strong / weak / overpaced<br>- what HR–power–cadence signals say about the body today<br>- whether efficiency was good or bad<br>- whether pacing was correct or too hard<br>- ONE actionable line for next time<br><br>Examples:<br><br>GOOD:<br>“Strong execution today. HR and power aligned well, drift low, cadence stable. Very efficient day. Same intensity next time.”<br><br>BAD:<br>“Low efficiency today. HR rose too fast for this power, drift high, cadence faded. Likely fatigue or underfueling. Reduce intensity next session.”<br><br>OVERPACED:<br>“You pushed above the intended zone. HR too high for given power and drift spiked. Good effort but too costly. Keep surges controlled next time.”<br><br>------------------------------------------------------------<br>4. FULL OUTPUT FORMAT<br>------------------------------------------------------------<br><br>After the Executive Summary, produce the deep-dive analysis:<br><br>1. Session Summary<br>   - name, date, duration, distance<br>   - avg power, NP, avg/max HR, avg cadence<br><br>2. Workout Type &amp; Inferred Intent<br><br>3. Power Execution<br>   - pacing, interval quality, variability, effort discipline<br><br>4. Heart Rate &amp; Aerobic Drift<br>   - numerical drift and interpretation<br><br>5. Cadence &amp; Neuromuscular Aspects<br><br>6. Fatigue &amp; Efficiency Indicators<br>   - HR–power alignment<br>   - durability<br>   - metabolic response<br>   - INCLUDE interpretation from private notes<br><br>7. Coach’s Verdict<br><br>8. Recommendation<br>   - specific next-session suggestion (Z2, SST, rest, cadence work, longer intervals, shorter recoveries, lower surges, etc.)<br><br>------------------------------------------------------------<br>5. GENERAL RULES<br>------------------------------------------------------------<br><br>• Always respond in the same language the user used last.  <br>• Never ask follow-up questions when the data is available.  <br>• This GPT NEVER handles multi-week planning.  <br>• This GPT ALWAYS does deep-dive analysis.  <br>• Provide specific performance judgments and next-session intensity suggestions.  <br>• Always read &amp; interpret private notes if present.</pre><h3>7. Authenticate Once</h3><p>After saving your GPT:</p><ul><li>Open it once</li><li>It will show a Strava login window</li><li>Approve access</li></ul><p>From now on, your GPT can pull your workouts automatically.</p><p>I generally use a simple prompt like <strong>“Judge my latest workout” </strong>after Zwift uploaded my ride to strava.</p><p>I will ask you to allow the action everytime or you just hit <em>always allow.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fwOyNU_xuz4GgOCxg8JAtg.png" /></figure><h3>That’s it: the Strava → ChatGPT bridge is done</h3><p>Your GPT can now:</p><ul><li>fetch activities</li><li>read power, cadence, HR streams</li><li>analyze trends</li><li>compare intervals</li><li>evaluate fatigue markers</li></ul><p>Everything else (the training philosophy, the analysis depth, progressions, block planning, etc.) is controlled by the Instructions section from Step 6.</p><p>See my example here (this is just the first snippet with the management summary, it goes into much more detail, skipped for privacy reasons ;) )</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YU6nLVdkHdTjhl2iuqggtg.png" /></figure><h3>Addition: API Limits, Long Workouts &amp; How Streams Behave</h3><p>Strava’s API has generous limits, but there are a few points worth knowing:</p><h3>Very long activities have extremely long streams</h3><p>A 6-hour ride can easily produce:</p><ul><li>20,000–30,000 power points</li><li>the same for heart rate</li><li>the same for cadence</li></ul><p>ChatGPT <em>can</em> load them but:</p><ul><li>large arrays slow down the response</li><li>sometimes the Streams endpoint fails halfway</li><li>extremely large responses may trigger ChatGPT’s safety or memory limits</li></ul><h3>2. What usually fails first</h3><p>Strava often returns:</p><pre>Error: request returned too much data to process</pre><p>That’s normal for:</p><ul><li>&gt; 3 hour indoor rides</li><li>&gt; 4 hour outdoor rides</li></ul><p>For me this doesn’t pose a huge challenge, as I’m mostly interested to judge my classic Zwift training sessions which for me are usually around 1,5–2,5 hrs.</p><h3>But: You can control granularity</h3><p>You can instruct your GPT to:</p><ul><li><strong>downsample</strong> streams (e.g., “use every 5th data point”)</li><li><strong>truncate</strong> (e.g., “limit to first/last 90 minutes”)</li><li><strong>summarize at segment level</strong> rather than second-by-second</li></ul><p>Example instruction you can add:</p><blockquote><em>“For rides longer than 3 hours, automatically downsample the time-series streams to 5-second or 10-second resolution before analysis.”</em></blockquote><p>ChatGPT handles that very well.</p><h3>4. When it’s better to analyze summary metrics</h3><p>If the GPT struggles to fetch full streams, it can fall back to summary data:</p><ul><li>NP</li><li>Avg HR</li><li>HRmax</li><li>HR drift based on laps</li><li>Power curves (summaries)</li><li>Suffer Score</li><li>Elevation</li><li>Average cadence</li></ul><p>And still deliver a high-quality coaching interpretation. But obviously it won’t be as detailed as the point-by-point interpretation</p><h3>5. You can add fallback instructions</h3><p>Example:</p><blockquote><em>“If streams fail, say so clearly and perform the best possible summary-based analysis instead.”</em></blockquote><p>This prevents frustration and model confusion.</p><h3>Have fun analyzing (and riding!)</h3><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=88084cc4c48f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Let Snowflake (Cortex) Auto-Doc Your Columns Like a Pro!]]></title>
            <link>https://medium.com/@david.klein_4417/let-snowflake-cortex-auto-doc-your-columns-like-a-pro-04f9fa6187e7?source=rss-3841e0d4c4fb------2</link>
            <guid isPermaLink="false">https://medium.com/p/04f9fa6187e7</guid>
            <dc:creator><![CDATA[David Klein]]></dc:creator>
            <pubDate>Fri, 21 Feb 2025 08:59:07 GMT</pubDate>
            <atom:updated>2025-02-21T08:59:07.039Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>Streamlining Database Documentation with Snowflake Cortex</strong></h3><p>Snowflake Cortex is changing how we handle data management tasks by harnessing the power of large language models (LLMs). There is endless applications of Cortex AI and LLMs out there today.</p><p>Today I want to focus a bit on a data management task; By using Snowflake Cortex, we can quickly generate meaningful, accurate descriptions for tables, views, and columns directly within Snowflake’s SQL-based environment. This helps reduce manual effort, improves data transparency, and enhances the discoverability of data, which is becoming increasingly important as LLMs enable users — traditionally distanced from the data — to access and leverage it more easily.</p><h3><strong>Generate Descriptions Easily in Snowsight UI</strong></h3><p>First of all, what I’m writing here might be obsolete in a few months — I definitely hope so! Because today already, you can <a href="https://docs.snowflake.com/en/user-guide/ui-snowsight-cortex-descriptions">generate descriptions directly from the Snowsight UI with just a few clicks.</a> By selecting “Generate descriptions” in the Table/Columns View, Snowflake automatically analyses the metadata and, if desired, sample data to generate a concise description for each column, table, or view. The descriptions are then saved in the COMMENTproperty, which can be viewed in Table Details or the DESCRIBE TABLE output. This simple UI feature is a fast and intuitive way to ensure that your tables and columns are well-documented, reducing the need for manual descriptions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9HF9WYNu46tsGy7F3Z-yRg.png" /></figure><p><em>“Generate descriptions” Button in Snowsight UI</em></p><h3><strong>Why Bulk Documentation Makes Sense for Large Datasets</strong></h3><p>In reality, managing thousands of tables in production means even simple tasks, like adding column descriptions, can quickly become time-consuming. Unfortunately, at the moment, we can’t automate this above-mentioned feature across multiple tables or bulk-edit it via code. Fear not! Snowflake Cortex steps in, allowing you to automate this task in bulk using SQL, making it easy to keep your database documentation up-to-date. That’s exactly why I’m outlining this workflow here.</p><p>By generating descriptions for all columns at once, you save time and ensure your database is consistently documented — no matter how large or complex the dataset. This scalability is especially important as data volumes continue to grow, making bulk documentation a key tool for maintaining an efficient, well-governed data ecosystem.</p><h3>Example of a Workflow, Step-by-Step</h3><p>Let’s dive into an example workflow. I’ll show you how to leverage Snowflake Cortex to create column descriptions, generate the corresponding ALTER statements, and then iterate through them to apply the changes to your database. This could later be wrapped in a stored procedure and a triggered task to automate the process easily.</p><h4>Step 1: Create and Populate the Table I want to describe</h4><pre>-- Create the table<br>CREATE OR REPLACE TABLE ANALYTICS.PUBLIC.CUSTOMER_FINANCIALS (<br>    CUSTOMER_ID INT AUTOINCREMENT PRIMARY KEY,<br>    CUSTOMER_NAME STRING,<br>    INDUSTRY STRING,<br>    REVENUE_USD NUMBER(12,2),<br>    PROFIT_MARGIN FLOAT,<br>    EMPLOYEE_COUNT INT,<br>    COUNTRY STRING,<br>    LAST_PURCHASE_DATE DATE,<br>    CREDIT_RATING STRING,<br>    ACCOUNT_MANAGER STRING<br>);<br>-- Insert sample data<br>INSERT INTO ANALYTICS.PUBLIC.CUSTOMER_FINANCIALS (CUSTOMER_NAME, INDUSTRY, REVENUE_USD, PROFIT_MARGIN, EMPLOYEE_COUNT, COUNTRY, LAST_PURCHASE_DATE, CREDIT_RATING, ACCOUNT_MANAGER) <br>VALUES<br>    (&#39;Acme Corp&#39;, &#39;Manufacturing&#39;, 50000000.00, 12.5, 1200, &#39;USA&#39;, &#39;2024-01-15&#39;, &#39;A&#39;, &#39;John Doe&#39;),<br>    (&#39;Beta Systems&#39;, &#39;Software&#39;, 25000000.00, 20.3, 500, &#39;Germany&#39;, &#39;2024-02-10&#39;, &#39;B&#39;, &#39;Jane Smith&#39;),<br>    (&#39;Gamma Innovations&#39;, &#39;Healthcare&#39;, 12000000.00, 15.8, 200, &#39;France&#39;, &#39;2023-12-05&#39;, &#39;A&#39;, &#39;Alice Brown&#39;),<br>    (&#39;Delta Dynamics&#39;, &#39;Retail&#39;, 78000000.00, 9.7, 3000, &#39;UK&#39;, &#39;2024-03-01&#39;, &#39;C&#39;, &#39;Bob Johnson&#39;);<br>...</pre><p>The table I’m using for this example workflow looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/998/1*SppgwSkk3fIdbuKHeGkjLw.png" /></figure><h4>Step 2: (Optional) Check the Auto-Generated Descriptions for All Columns</h4><p>With your table in place, the next step is to generate descriptions for all the columns and check if they makes sense at all — after all LLMs can hallucinate. Snowflake Cortex enables us to leverage its COMPLETE function, which can generate text descriptions based on a prompt. Here, I’ll use this function to automatically generate column descriptions for all columns in the CUSTOMER_FINANCIALS table. This step is optional of course, but I personally like to build these things out step-by-step to avoid complex debugging later on.</p><pre>WITH ai_descriptions AS (<br>    SELECT <br>        c.column_name,<br>        SNOWFLAKE.CORTEX.COMPLETE(<br>            &#39;mistral-7b&#39;, <br>            &#39;Provide a concise description (≤10 words) for the following database column. <br>            Do not repeat the column name or mention the data type. <br>            Example: &quot;Total amount paid&quot;, &quot;Date of last transaction&quot;. Column: &#39; || c.column_name<br>        ) AS generated_description<br>    FROM information_schema.columns c<br>    WHERE c.table_schema = &#39;PUBLIC&#39; <br>    AND c.table_name = &#39;CUSTOMER_FINANCIALS&#39;<br>)<br>SELECT * FROM ai_descriptions;</pre><p>This query utilises the SNOWFLAKE.CORTEX.COMPLETE function to generate descriptions for every column in the table. You can see that the COMPLETE function takes a model (mistral-7b in this case) and a prompt that requests a concise description for each column — I personally found mistral-7b to work quite well and efficiently in this type of task. (Feel free to play around with the prompt and be as verbose as needed for the depth of detail for your description.) The results will include the column name and the generated description.</p><p>The descriptions that Cortex created for me look roughly like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aNRTeMJOPz_sHd3kzqQisA.png" /></figure><h4>Step 3: Create the Alter Statements</h4><p>Once the descriptions are generated, you can use them to update the table’s column comments. Just as in Step 2, I utilise our prompt and Cortex to generate the descriptions and wrap them in the correct ALTER statements:</p><pre>CREATE OR REPLACE TEMP TABLE temp_alter_statements AS<br>SELECT <br>    &#39;ALTER TABLE IDENTIFIER(&#39;&#39;&quot;ANALYTICS&quot;.&quot;PUBLIC&quot;.&quot;CUSTOMER_FINANCIALS&quot;&#39;&#39; ) <br>     ALTER COLUMN &quot;&#39; || column_name || &#39;&quot; COMMENT &#39; || <br>     &#39;&#39;&#39;&#39; || REPLACE(comment, &#39;&#39;&#39;&#39;, &#39;&#39;&#39;&#39;&#39;&#39;) || &#39;&#39;&#39;&#39; AS ALTER_SQL<br>FROM (<br>    SELECT <br>        column_name,<br>        SNOWFLAKE.CORTEX.COMPLETE(<br>            &#39;mistral-7b&#39;, <br>            &#39;Provide a concise description (≤10 words) for the following database column.<br>             Do not repeat the column name or mention the data type. <br>             Example: &quot;Total amount paid&quot;, &quot;Date of last transaction&quot;. Column: &#39; || column_name<br>        ) AS comment<br>    FROM information_schema.columns <br>    WHERE table_schema = &#39;PUBLIC&#39; <br>    AND table_name = &#39;CUSTOMER_FINANCIALS&#39;<br>);</pre><p>This query generates a temporary table with all the ALTER SQL statements that will update the column comments in the CUSTOMER_FINANCIALS table. The contents of the table will be simple statements, looking like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yOsVh9RY8CNuy7Amu-dwlQ.png" /></figure><h4>Step 4: Execute the Statements</h4><p>In this final step, I use a <strong>cursor</strong> to iterate over the ALTER statements stored in temp_alter_statements. The cursor fetches each ALTER_SQL and executes it using EXECUTE IMMEDIATE. This process can be wrapped in a <strong>stored procedure</strong>, allowing for the bulk application of column descriptions efficiently with a single call.</p><pre>DECLARE cur CURSOR FOR <br>    SELECT ALTER_SQL FROM temp_alter_statements;<br>BEGIN<br>    FOR rec IN cur DO <br>        EXECUTE IMMEDIATE rec.ALTER_SQL;<br>    END FOR;<br>END;</pre><p><strong>Last thing to note </strong>— I’m really not that deep into SQL, so there might be a more elegant way to achieve this or automate it further. Feel free to share your thoughts in the comments!</p><h3>The Magic Behind Snowflake Cortex: Reducing Manual Work</h3><p>By automating the generation and application of column descriptions, Snowflake Cortex drastically reduces the time and effort traditionally required for this task. What once required manual writing of descriptions or using external tools can now be done directly in SQL with Snowflake’s Cortex-powered functions.</p><p>The beauty of this solution is that it doesn’t require deep knowledge of machine learning models or Python scripting. Everything happens within the familiar SQL environment of Snowflake, making it accessible for anyone with SQL experience.</p><h3>What’s Next for Snowflake Cortex?</h3><p>As Snowflake Cortex continues to evolve, we can expect to see even more powerful tools for data management tasks. The ability to automate data documentation is just the beginning. In the future, Cortex will likely help simplify other data management tasks, making the Snowflake platform even more powerful and reducing the need for manual data operations. With Snowflake’s integration of LLMs, we’re witnessing a new era of AI-driven data management that promises to make complex workflows easier and more efficient than ever before.</p><p>Imagine a future where much of your data management tasks — from data cleaning to data enrichment and beyond — are automated by AI. Snowflake is making this vision a reality, and it’s just the beginning.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=04f9fa6187e7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Transforming Lead Nurturing with Large Language Models in Snowflake Cortex]]></title>
            <link>https://medium.com/@david.klein_4417/transforming-lead-nurturing-with-large-language-models-in-snowflake-cortex-368b59446574?source=rss-3841e0d4c4fb------2</link>
            <guid isPermaLink="false">https://medium.com/p/368b59446574</guid>
            <dc:creator><![CDATA[David Klein]]></dc:creator>
            <pubDate>Mon, 12 Feb 2024 06:18:00 GMT</pubDate>
            <atom:updated>2024-02-12T08:44:17.423Z</atom:updated>
            <content:encoded><![CDATA[<p>In the realm of sales and marketing, managing leads effectively is paramount for business growth. <a href="https://en.wikipedia.org/wiki/Lead_management">Lead management</a> encompasses the process of identifying potential customers, nurturing their interest, and guiding them through the sales funnel. However, this task often poses challenges due to the sheer volume of data and the need for personalized engagement.</p><p>Automation with advanced language models, such as Large Language Models (LLMs), offers a transformative solution to these challenges. By harnessing the power of LLMs, businesses can streamline lead management processes, automate repetitive tasks, and gain valuable insights from vast amounts of data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/620/1*zP9IAPxBk_4C_DZjgtaJtQ.jpeg" /></figure><p>LLMs excel at understanding and analyzing natural language, enabling them to extract meaningful information from lead interactions, classify prospects based on their interests and behaviors, and generate personalized responses. This automation not only enhances efficiency but also allows sales and marketing teams to focus on high-value activities, such as building relationships and closing deals.</p><p>This blog is going to explore the critical role of <strong>lead nurturing in sales and marketing and how automation powered by advanced language models</strong>, such as Large Language Models (LLMs), can revolutionize this process. We’ll discuss the challenges businesses encounter with lead management and how leveraging LLMs for automation can address these obstacles. Additionally, we’ll highlight the benefits of utilizing Snowflake’s LLM capabilities to enhance lead nurturing efforts. Stay tuned to discover how this innovative approach can drive efficiency, personalization, and growth in your sales pipeline.</p><p>Let’s imagine something like this, fully machine generated automatically sent to your prospects!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/798/1*r6uxye2CiCor21L9D3V5Qg.png" /></figure><h3>What are LLMs — for those who have missed the last year in tech industry headlines…</h3><p>Large Language Models (LLMs) are advanced artificial intelligence systems designed to understand and generate human-like text based on vast amounts of training data. These models have revolutionized natural language processing tasks and are increasingly being applied across various domains. Here are some common use cases for LLMs:</p><ul><li>Generative Tasks</li><li>Summarization</li><li>Rewriting</li><li>Searching</li><li>Question Answering</li><li>Clustering</li><li>Classification</li></ul><h3>Snowflake Cortex: Empowering SQL Users with AI Capabilities</h3><p><a href="https://www.snowflake.com/blog/use-ai-snowflake-cortex/">Snowflake Cortex</a> revolutionizes AI accessibility by bringing the power of generative AI and large language models (LLMs) to users of all backgrounds within the Snowflake platform. With a suite of serverless functions, Cortex enables seamless analytics and AI app development, offering access to specialized ML and LLM models tailored for specific tasks. By integrating these capabilities into Snowflake’s unified governance framework, Cortex ensures secure data management while enhancing user experiences with intuitive features like Snowflake Copilot and Universal Search. This groundbreaking service democratizes AI, empowering organizations to unlock dynamic insights from their data assets with ease.</p><p><strong>The coolest thing about Cortex</strong>, especially for SQL users like myself who aren’t deeply into Python or have extensive backgrounds in Data Science or ML Engineering, <strong>is its complete reliance on SQL</strong>. The magic lies in the simplicity of SQL functions such as (snowflake.cortex.complete<strong>, </strong>snowflake.cortex.sentiment , snowflake.cortex.summarize and more), which I’ll demonstrate shortly.</p><h3>The Scenario: Everyday CRM Data at NextGen Innovations</h3><p>At NextGen Innovations (a fictitious company of course…), we specialize in providing advanced solutions for data analytics, AI, and cybersecurity. Our CRM system is filled with valuable data, including leads from companies across the globe. These leads, like those from Siemens AG in Germany, Airbus SE in France, and Microsoft Corporation in the United States, represent typical entries in CRM databases.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*I1zjWZ93rGINdHya4eYE6Q.png" /><figcaption>Snapshot of our CRM Data: Initial Lead Details in the table “leads”</figcaption></figure><p>In a traditional setup, our Business Development Representatives (BDRs) and Sales Development Representatives (SDRs) would utilize this CRM data for lead nurturing. They’d engage with leads based on their activities, such as downloading eBooks, attending events, or registering for webinars, to move them through the sales funnel.</p><p>For instance, if a lead like John Smith, a Data Analyst at Siemens AG, downloads an eBook on “Mastering Data Science,” our team would follow up with targeted communications tailored to his interests. This might involve sharing additional resources, scheduling personalized demos, or inviting him to relevant events.</p><p>The goal is to build relationships with leads, understand their needs, and guide them towards becoming qualified opportunities for our sales team. Traditional lead nurturing relies heavily on manual efforts to identify, prioritize, and engage with leads based on their behaviors and interests within the CRM system</p><h3>Snowflake.Cortex.Complete: Leveraging General-Purpose LLMs in Snowflake</h3><p>In the following section, we’ll explore the functionality of snowflake.cortex.complete within Snowflake Cortex and its implications for your lead nurturing efforts. Snowflake Cortex harnesses the power of Large Language Models (LLMs), with the foundational model being Llama, for various general-purpose tasks, including text completion and generation. While Llama is renowned for its prowess in generative tasks, it&#39;s essential to note that within Snowflake, <strong>Cortex is currently in a private preview phase</strong> (this being written in Feb 24).</p><p>Snowflake.cortex.complete allows users to generate text completions based on prompts provided, leveraging the capabilities of LLMs like Llama 7b or Llama 70b. However, <strong>it&#39;s crucial to understand that these models are not yet tailored to individual company data</strong>. As Cortex continues to evolve within Snowflake, future iterations may include fine-tuning LLMs on company-specific data, offering more precise and tailored outcomes for lead nurturing endeavors.</p><p>The cortex functions can be use as such:</p><pre>SNOWFLAKE.CORTEX.COMPLETE(model, prompt) -- returns string</pre><ul><li>model: A string containing the name of the model to be used, either &quot;llama2-70b-chat&quot; or &quot;llama2-7b-chat&quot;.</li><li>prompt: A string containing the prompt to be used to begin generating the response. In an interactive chat scenario, this would be something the user typed; here, it is a plain-English description of what you want.</li></ul><h3>Part 1: Using the LLM for Data Extraction, Enrichment &amp; Classification</h3><p>In this phase, we embark on a comprehensive process to extract, enrich, and classify data from our CRM systems. Our primary objectives are to <strong>assign an industry classification</strong> based on the company’s profile, <strong>enrich existing company information</strong>, and <strong>extract language profiles</strong> based on the company’s headquarters. Additionally, we aim to <strong>identify the area of focus for each prospect</strong> by analyzing their interactions with our company’s content and events. All this information will later on help us to prompt the LLM to automate a follow up email to our prospects.</p><p><strong>1. Data Extraction:</strong> We begin by extracting pertinent data from our CRM systems, including lead information, company profiles, and interaction histories.</p><p><strong>2. Data Enrichment:</strong> Once extracted, we enrich the data by supplementing it with additional information sourced from external databases and APIs. This may include details such as company size, revenue, industry trends, and market insights.</p><p><strong>3. Classification:</strong> Utilizing the enriched data, we perform zero-shot classification to categorize leads based on various criteria like industry verticals, company size, and engagement levels. This segmentation enables tailored approaches to effectively nurture leads.</p><p>By combining data extraction, enrichment, and classification, we lay the groundwork for a highly personalized and effective lead nurturing strategy. This phase sets the stage for the subsequent generation of dynamic content and engagement strategies tailored to each lead’s unique profile and preferences.</p><p>We will use different approaches in the prompting for classification to show you different alternatives. You can either hardcode the classes you want the LLM to use in the prompt or use an additional SQL query to list (&quot;listagg()&quot;) the categories/classes the LLM is supposed to base it’s decisions on. For the latter approach we will use this data in the table industries.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*HOrLttykbHakBLBiWiUxqA.png" /><figcaption>industries table (n=20), standard industry classifications</figcaption></figure><p>Ok, let’s get dirty and dive into the code for the above mentioned tasks. We’ll incorporate all of the tasks into one prompt to maximize the output with limited amount of tokens and interations. The LLM will be prompted to give us a JSON structure as response so we can easily parse it into new columns for subsequent use cases.</p><pre>UPDATE leads<br>SET cortex_json = <br>    SNOWFLAKE.CORTEX.COMPLETE(<br>        &#39;llama2-70b-chat&#39;,<br>        CONCAT(<br>            &#39;You are a classification and information extraction and enrichment bot.<br>            If you are unsure, return null. <br>                For industry identify the companies industry chose exclusively from the list given below (choose only one),<br>                for profile single-sentence (maximum 12 words) business description<br>                for language extract the language in format &quot;en&quot; for english speaking countries, &quot;de&quot; , &quot;fr&quot; etc from the information of the country column you are given<br>                for area of focus try to match into the categories: Artifical Intelligence, Cybersecurity, Applications, Internet of Things (if you choose multiple write them in json array format)<br>            Respond exclusively in JSON format without any additional comments.<br><br>            {<br>            industry: &#39;,<br>            (SELECT LISTAGG(industry_name, &#39;, &#39;) WITHIN GROUP (ORDER BY industry_name) <br>             FROM industries),<br>            &#39;,<br>            profile: <br>            language:<br>            area_of_focus:<br><br>            }<br>        &#39;<br>        , t.company, t.country<br>        , &#39;Return results&#39;<br>        )<br>    ) <br>FROM <br>    leads t;</pre><p>As you can see, the prompt includes the &quot;listagg()&quot; command to pull the information for industry classifications from a different table, while thearea_of_focus categories are hardcoded into the prompt, to give you an idea of various approaches possible. Similarly, we also allowed multiple values for the area of focus in order to see how well the LLM plays with arrays with multiple elements.</p><p>As always, the precision and quality of our responses hinge on the level of detail provided in the prompt. Therefore, we strive to be as prescriptive and comprehensive as possible in crafting our prompts to ensure optimal results, which look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lOXnV4n-VXY8VPxIB0knQw.jpeg" /><figcaption>This is a preview of the ten first rows passed through our Cortex Function</figcaption></figure><pre> {<br>  &quot;industry&quot;: &quot;Manufacturing&quot;,<br>  &quot;profile&quot;: &quot;Airbus SE: A global leader in aeronautics, space, and related services.&quot;,<br>  &quot;language&quot;: &quot;fr&quot;,<br>  &quot;area_of_focus&quot;: [&quot;Artificial Intelligence&quot;, &quot;Cybersecurity&quot;]<br>}</pre><p>Pretty cool I guess! The JSON has 4 fields: industry, company profile, language profile, and area of focus. The information comes from the general knowledge of our LLM. Based on the time it was trained, this might of course be a bit outdated, but for this use case it works quite well.</p><p>No we can easily parse this information into new columns if we like.</p><pre>UPDATE leads<br>SET <br>    industry = TRIM(PARSE_JSON(cortex_json):industry, &#39;&quot;&#39;),<br>    profile = TRIM(PARSE_JSON(cortex_json):profile, &#39;&quot;&#39;),<br>    language = TRIM(PARSE_JSON(cortex_json):language, &#39;&quot;&#39;),<br>    area_of_focus = TRIM(PARSE_JSON(cortex_json):area_of_focus, &#39;&quot;&#39;);</pre><p>Which leaves us with this structure. Certainly we wouldn’t need to create these new columns, but I always find it easier to work through this step-by-step while engineering this kind of workflow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QRJ-X-8KiZSsatHsjsa6eg.png" /></figure><h3>Part 2: Automating Personalized Engagement with Snowflake Cortex</h3><p>In this phase, we harness the power of generative AI within Snowflake Cortex to craft personalized follow-up outreach to our prospects. While the first phase focused on data extraction, enrichment, and classification, this stage delves into content generation using the Llama 2 model.</p><p>Llama 2 stands out for its prowess in content creation, making it the ideal choice for crafting engaging and personalized follow-up communications. Fine-tuned models like Llama 2 have demonstrated their ability to expedite content generation across various platforms, from web content to social media posts and beyond. Moreover, Llama 2 can aid in brainstorming new ideas by suggesting keywords, topics, or formats tailored to our preferences.</p><p>By leveraging Llama 2 within Snowflake Cortex, we can automate the creation of dynamic and personalized follow-up outreach to our prospects. This enables us to engage with leads in a meaningful and tailored manner, increasing the likelihood of conversion and fostering stronger customer relationships. For this we will use the information we extracted, classified and enriched in the first part. For the second part we will again use Snowflake.cortex.complete with a new prompt in order to create our outreaches.</p><pre>UPDATE leads<br>SET OUTREACH=<br>    SNOWFLAKE.CORTEX.COMPLETE(<br>        &#39;llama2-70b-chat&#39;,<br>        CONCAT(<br>            &#39;You are an email-writing robot. Your task is to compose an email to prospects of our company called NextGen Innovations. Address them by their name (&#39;, t.NAME, &#39;)<br>            They have shown interest in &#39;, t.AREA_OF_FOCUS, &#39;. The company&#39;,t.COMPANY,&#39; is in the industry of &#39;, t.INDUSTRY, &#39;, and their major business is &#39;, t.PROFILE, &#39; the person you are writing to works as&#39;,t.TITLE,&#39;. <br>            They have been engaging with our product via: &#39;, t.LAST_INTEREST, &#39;, the date they have been enagaging is: &#39;, t.LAST_MARKETING_ACTIVITY, &#39;. <br>            However, do not use the exact date but refer to the relative to today&#39;,CURRENT_DATE,&#39;such as (&quot;I have seen you downloaded ... yesterday, a couple of days ago, a week ago, a couple of weeks ago, etc.&quot;). <br>            Use all the information such as industry, their title, their areas of focus, the content they have been looking at to write a compelling email asking about typical challenges they would face in the industry and their job position and how our product would solve this.<br>            Ask them if they would be interested in learning more, add a call to action, etc. End the email with greetings from the person who owns the contact, which is: &#39;, t.LEAD_OWNER, &#39; and add their title after a comma, Business Development Associate.<br>            Respond exclusively in JSON format without any additional comments.Be creative in writing style and feel free to be a bit challenging.<br><br>            {<br>            subject: <br>            email_body:<br>            }&#39;<br>            <br>        )<br>    )<br>FROM <br>    leads t;</pre><p>Similar to step 1, we prompt Cortex to deliver a JSON output, but this time, the prompt is tailored specifically for crafting personalized email outreach. The prompt includes various columns from our previous step, such as the prospect’s name, company, industry, job title, areas of focus, and engagement history. Additionally, we include the current date to provide context for the LLM, as it does not inherently understand time. This ensures that the generated emails feel timely and relevant to the recipient.</p><p>The prompt itself is structured to guide the LLM in composing a natural and compelling email. It instructs the model to address the prospect by name, acknowledge their interest and engagement with our company, and tailor the email content based on their industry, job position, and areas of focus. The email prompts the prospect to consider typical challenges they face in their industry and role, highlighting how our product can address these challenges. A call to action is included to encourage further engagement, and the email concludes with greetings from the lead owner, adding a personal touch to the outreach.</p><p>Please note: Due to the nature of LLMs and their propensity for generating diverse responses, the output of the Cortex function may vary with each iteration. This means everytime we iterate through the code, the responses are a bit differently.</p><p>This leaves us (for example) with this output:</p><pre> {&quot;subject&quot;: &quot;Revolutionizing Data Science with NextGen Innovations&quot;,<br>  &quot;email_body&quot;: &quot;Dear Emma Johnson,<br><br>I hope this email finds you well. I came across your profile and noticed that you have been engaging with our product, specifically in the areas of Artificial Intelligence, Cybersecurity, and Applications. As the Head of Data Science at Airbus SE, I&#39;m sure you&#39;re constantly looking for ways to stay ahead in the industry.<br><br>I wanted to reach out and ask if you&#39;ve encountered any challenges in your role, such as managing large datasets, ensuring data security, or struggling to find the right tools to support your team&#39;s work? Our product has been designed to address these exact pain points, and I believe it could be a valuable addition to your toolkit.<br><br>Our AI-powered solutions have been developed to help data scientists like yourself streamline their workflows, improve data quality, and enhance decision-making capabilities. Additionally, our cybersecurity features ensure that your data is protected from unauthorized access and breaches.<br><br>I&#39;d love to schedule a call to discuss how our product can support your team&#39;s efforts and help you overcome any obstacles you may be facing. Would you be interested in learning more?<br><br>Please let me know if you&#39;re available this week or next, and I&#39;ll make sure to schedule a time that works for you.<br><br>Best regards,<br>Sophia Müller, Business Development Associate<br><br>P.S. I noticed that you downloaded our whitepaper on AI and Machine Learning yesterday. Great timing! I&#39;m excited to share more insights with you on how our product can help you achieve your goals.&quot;<br>}</pre><p>I think the output isn’t quite that bad. Sure, a good BDR/SDR cannot be easily replaced by an LLM, as there’s a human touch and nuanced understanding that an AI model may lack. However, what’s cool about the output is how it captures the essence of a personalized outreach. It references specific actions taken by the prospect, such as downloading a whitepaper ‘yesterday,’ and addresses potential challenges they may face in their industry or role, even if at a high level. This level of detail adds authenticity to the communication and can help foster a deeper connection with the recipient. Might be enough to “nurture” that lead and get them engaged again.</p><h3>Looking Ahead: Current Limitations and Outlook</h3><p>Cool stuff in my opinion. But as always, it’s important to consider some limitations and potential future outlooks for the platform.</p><p>One current limitation is that the LLM model used in Cortex is not fine-tuned on company-specific data. While this may impact the specificity of generated content, it also highlights the platform’s versatility in handling diverse datasets. As Cortex continues to evolve, future iterations may include fine-tuning LLMs on company-specific data, offering more precise and tailored outcomes</p><p>Additionally, I’ve experimented with theSnowflake.cortex.translate function to regionalize outreach based on language extracted in previous steps. However, the current model quality may not suffice for highly personalized, coherent texts. Some text bits were translated weirdly, but honestly I didn’t go further down that road for long, might be something for a future blog post and further down the load the translate function. To Snowflake’s defense, remember these functions are all in Private Preview currently.</p><p>In considering alternative avenues, Snowflake’s introduction of<a href="https://docs.snowflake.com/en/developer-guide/snowpark-container-services/overview"> Snowpark Container Services</a> presents an intriguing prospect. This fully managed container offering facilitates the deployment, management, and scaling of containerized applications, hence LLMs, within the Snowflake platform. While this avenue would require self-hosting it in Snowflake, it offers a pathway to circumvent current limitations by providing greater flexibility and control over model deployment processes.</p><h3>Conclusion</h3><p>Snowflake Cortex LLM Functions democratize AI by making it accessible to everyone through the simplicity of SQL. This accessibility means that even those without extensive data science backgrounds can leverage powerful AI capabilities to drive business growth and innovation. Imagine the possibilities for automation in lead nurturing, marketing outreach, and beyond — it’s a game-changer in terms of efficiency and effectiveness.</p><p>Moreover, Snowflake’s platform ensures that these capabilities can scale effortlessly to handle vast amounts of data. With Snowflake’s built-in governance and security features, organizations can trust that their data remains protected while unlocking the full potential of Cortex LLM Functions. It’s not just about utilizing AI — it’s about doing so in a way that is scalable, secure, and aligned with the highest standards of governance.</p><p>Let’s automate the s***t out of stuff!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=368b59446574" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>