Scheduled Functions (Cron Jobs) With Netlify
Netlify just announced support for scheduled functions, which are basically serverless cron jobs. In this episode, Jason will explore the new functionality and see how it all works!
Links & Resources
- https://www.netlify.com/blog/quirrel-joins-netlify-and-scheduled-functions-launches-in-beta
- https://ntl.fyi/sched-func
- https://crontab.guru/
- https://quirrel.dev/
- https://developers.notion.com/reference/intro
- https://icanhazdadjoke.com/
- https://github.com/nodejs/node/pull/41749
- https://www.learnwithjason.dev/let-s-learn-blitz-js
- https://www.learnwithjason.dev/serverless-functions-and-typescript-on-netlify
- https://www.learnwithjason.dev/power-up-react-with-typescript
- https://www.learnwithjason.dev/let-s-learn-typescript
- https://www.netlify.com/blog/2021/03/31/test-drive-netlify-beta-features-with-netlify-labs/
Full Transcript
Click to toggle the visibility of the transcript
Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.
JASON: Hello, everyone, and welcome to another episode of Learn With Jason. Today on the show we are going solo. So I am going to be playing with a new feature that I'm super excited about, Netlify just dropped today actually. Scheduled function. So this is a big one. We now have Cron Jobs at Netlify and I'm completely thrilled about this. This is a big deal for a lot of reasons. First and foremost, Cron Jobs are one of those things that you don't know you need it until you need it. And, yeah, what Brian's saying in the chat, no more IFTTT. If this than that is an amazing tool. It is an amazing tool but it is really nice to not have to use those tools. Especially if you're already building out APIs locally and all those kinds of things. So this is a big one. I'm also just really excited about it because, like, I regularly find myself wanting just a way to post a message every once in a while, to remind me of a thing or to, you know, go check some details. Maybe I want to rebuild my site at a ‑‑ on a daily basis because I've got some details that change, right? And being able to do that without having to reach for third‑party tooling is a really, really good thing. Oh, what's up, Ben? Welcome, team. Yes, Chris, I'm going no glasses. I started playing with my light and I get weird reflections on my glasses, so I think I might just be somebody who doesn't wear glasses now. I don't know. My vision is not actually that bad. I just wear the glasses because, you know, when I was a kid, I had whatever, 20/15 vision where you could see, like, further than they would expect you to. As I've gotten older, I only have 20/20, so ‑‑ but, yeah, contacts for ‑‑ if I think if my ‑‑ if I had worse vision, I would probably be looking for contacts, but, honestly, I wear the glasses because I feel like they've become part of who I am, but maybe no more. Maybe now I'm a no‑glasses person, who knows. All right. We got everybody. Hey, Cassidy. How are you doing? Welcome everybody. Thanks for hanging out. I am really, really excited to play around with this today. So, here's what I'll ask. Chat, what are y'all currently using, like ‑‑ well, first, two questions. First, who is using Cron Jobs today? Let me get a "W" in the chat, a boop in the chat if you are using Cron Jobs today. I'm seeing a couple of people. Yep. Yep. Lots of people in here. Ooh, Integra Map. That's a good one. Lots and lots of people using them. Cassidy is on a mission now. Who's gonna help her. Ben is here to assist. Are we gonna get the buried in boops? Are we doing it. David is planning to use Cron Jobs later. If you're not familiar with Cron Jobs, that joke will be funny shortly. I think you're ‑‑ are you gonna do it? Got to work harder than that chat. Get there. Get there. Get there. (Laughter)
Oh, you're almost there. You're so close. You're so close to having this done. Oh, Brian's in. Okay. Here we go. Here we go. (Laughter)
I shortened the time‑out a little bit. This is now much harder for y'all. Just what happens is every time you fill the screen with boops, I shorten the time‑out just a little bit more. (Laughter)
You know what? This is great. Okay. So, let's talk a little bit about this. So, we see a lot of people are using the ‑‑ a lot of people are using Cron Jobs. I think that's really exciting. I am really, really into Cron Jobs as just a convenient way to do work at a regular basis. Oh, here comes the stampede. And for some reason, my stampede is so quiet. Yeah, that was too quiet. I had my sound effects turned down to negative something. So Cron Jobs are the sort of thing that, like, I just use them for, you know, I listed a handful of things. I'd love to hear from you, though. What are you using Cron Jobs for? What are y'all, when you're putting these sort of things together, what sort of things do you need to do on a regular basis? Brian's talking about a YouTube API thingy that could have used a daily ingest. That's a great, like, that is a great use case. If you've got third‑party content and you want to pull it in in together, a Cron Job is a great way to do that. You can hit that API and bring the content back in. Tony is using it to bring the data back into a GitHub repo. Process invoices. Magento is using Cron Jobs a lot. Inventory checks and fulfillment checks and that sort of thing. A great way to skirt API limits. That's actually a good point. If you were trying to use something on a free tier and you don't want to, like, burn through your API limits, you can make fewer API calls at a regular basis and kind of batch things. A good use case, I'm seeing Brian talking about sending daily activity updates via email. I personally don't want an email every time an activity happens. I want, like, a digest of activity. So a Cron Job is a great way to do that. I can say I want a daily digest and that Cron Job would then collect activity and send me one email at a regular interval that has everything that happened since the previous email was sent. That, to me, is way less noisy than whenever somebody does something in the app, send me an email. Those are really, really useful ways of doing it. See, static site generators plus API calls daily or hourly to rebuild the site. That is a great way, like, a good example would be actually the Learn With Jason site. The episodes as they go from scheduled to live to an episode archive, those right now, we end up editing things in Sanity and that triggers builds and kind of keeps the site up to date. We don't need to really worry about it because all that sort of stuff happens. What happens if I forget and an episode gets cancelled and I forget to take it off the schedule? If the site rebuilt on a daily basis, that scheduled episode would disappear and wouldn't confuse people. Charlie devs is saying they're an underrated tool. 100% agreed. Download an audio clip from the stream at a certain time of day and upload it. Absolutely, yes. So many good things. Yeah, fetch data to populate business KPIs on a dashboard. Ooh, that's a good one. Ooh, okay, maybe that's what we should build today, is a way to pull in something and, like, put it somewhere else. I've been playing with a Notion API. That's pretty exciting. Maybe we can do a little bit of that. Okay. I like this. I want to do this. Let's do it. All right. So, first, let's do a quick switch over to the pairing ‑‑ I guess it's not really pairing anymore. Oh, Charlie, thank you for gifting a sub. Thank you very much for that. I've lost my train of thought. What was I talking about? Let's talk about the live streaming. So we're live right now on Twitch. Thank you all for being here. I'm really enjoying that. I have Jordan with me from White Coat Captioning, and Jordan is writing down everything that we say today so that y'all can keep up. Making the show a little bit more accessible. That is made possible through the support of our sponsors. We've got Netlify, NX, a mono repo tool, and backlight, the design system management tool, all kicking in to make this show more accessible to more people. Which I really appreciate it. It also helps me keep the lights on. I have a team that helps me with video production, keeping the site up to date, all of that is made possible through the support of sponsors and you, chat. Everyone who subscribes. Charlie, you gifting a sub. Brian just gifted a bunch of subs. Thank you so much. All of that helps me keep the show running. So thank you all so, so much. We are very close to a hype train, it looks like. So if anyone wants to make noise, if you've been on the fence about whether or not you want to be a subscriber to this channel, which would give you access to all those boops and making noise and, you know, all those things, now is the time. So, let's talk a little bit about what happened today. So, Netlify just announced this feature. We have scheduled functions. They're in beta right now. And that is because we acquired a company called Quirrel. So, Quirrel is a company created by Simon, if you follow Blitz js or in the Next.js world is a very active contributor there. Thank you very much for the sub. Oh, boy, everyone is gifting subs. Thank you. Thank you. Thank you. And Brandon gifting subs. Y'all are the best. Thank you very much. Just keeping them going. And so Quirrel was a way for you to set up Cron Jobs kind of externally. It was a third‑party tool. So it would kind of give you the ability to use any serverless platform and we liked this so much at Netlify that we just got in touch and we were like, hey, why don't we just, you know, make this official? So, Simon joined us. He's now part of our team. As part of that, we have worked it in. It's now part of the core Netlify serverless offering and we have the ability to do scheduled functions. So what I want to do today is I want to actually try this out. So let's give it a shot. Rhyme gonna go in and let's make a new repo here. I'm gonna ‑‑ let's see. Call it Netlify scheduled functions. And I'm gonna go into that. And let's npm init. I'm just going to say yes. Good. And then I want to ‑‑ what else do I want to do? I want to git init. Good. All right. Those are all the things I need, I think. And from here ‑‑ oh, you know what I should do instead? No, I'm just gonna use regular old functions. We're not gonna, like, build a site for this. It's just gonna be for some functions. And I have the docs here. The docs are available at the ‑‑ this short link here that will take you to the GitHub. Let me also link to this blog post, if anybody wants to check it out. And then this is Quirrel, which is the technology that we built on top of for the scheduled functions. Let's see. Hype train level 4. Dang, y'all. We are really close. Thank you, Adrian and Adarza for subscribing. Thank you again to everybody who is submitting. I don't know if we've ever been past level 4. This might be the highest score we've gotten on a hype train ever, actually. So thank you all very much. I really appreciate it. This is fun for me. Oh, we're going more. Ben is ‑‑ Ben just gifted one. And we're on to level 5. Is there any level higher than 5? I feel like Cassidy has been through the full hype train cycle before. What is the ‑‑ what's the highest you can get? 5 is the highest. Okay. I'm getting confirmation. So, this is ‑‑ oh, Chris out here with all the ‑‑ jeez, just killing it. Thank you very much for gifting all those subs. And is that ‑‑ are we doing it? PoofyChew. Is that Jessica? What is up? How have you been? I used to work with Jessica back at IBM. Yeah, all sorts of great, great things going on, and we just hit the full hype train, y'all. All right. Is that ‑‑ that's the whole thing, right? We just ‑‑ we hype accomplished. We've gone full hype. Is that it? Level 5 complete. Did they add a level 6? Where are we at? 100%. All right, y'all. Thank you all so much. We have, let's see, less than four minutes to see how far beyond 100% we can push this. If anybody is still on the fence, you can use your Amazon Prime account to sub, and that also goes to help the show. So thank you all very, very much for all of the help. Thank you so much for just all the support. This is all very, very helpful to me and, you know, also just kind of makes my day when everybody, oh, you like me, you really like me. (Laughter)
All right. So, let's actually use this thing. So I'm getting into the docs here. This is the scheduled functions documentation. This is the new feature built by Netlify, just dropped today. It's now in beta. So it's in Netlify Labs. So if y'all aren't familiar with Netlify Labs, I would highly encourage you to check this out because what Netlify Labs will let you do is test drive new stuff. Let me share this link here. This is very cool. Thank you very much, Jessica, for the sub. I appreciate it. And let's dive into the app. And the way you can find Labs is you can go into Netlify Labs here and it's gonna show you options. Things that you can enable experimentally, right? So scheduled functions is the one that we want, so I'm gonna click "enable" to get that turned on, and then I'm gonna come back here. And let's see. So I can get started. I went to Netlify Labs and I enabled the feature. So then I'm going to have the option to enable scheduled functions on individual sites within your Netlify organization by navigating to the functions tab and clicking "enable scheduled functions." Okay so the first thing I want to do is I'm going to create a site. So let's start by opening this up. I'm just gonna make a basic function. And the function that I want to make is gonna live at Netlifyfunctions and we'll call this one hello world. I'm gonna use TypeScript because I want to get the ‑‑ there is type safety that we can get, but also it lets me use modern syntax. I can export const handler. That is going to let me send back one of these. Our status code will be 200. This is our basic hello world function. And with that, I'm going to git status. I'm going to git add all. Great. Git commit. And we'll say add a hello world function. All right. And then I'm going to ‑‑ let's push. Did I create this repo? I didn't, did I? So GitHub repo create. And I'm gonna use this repo name here. This is the GitHub CLI, which I love. Let's drop that in here. Good submarine placement, Charlie. Thank you. Yeah, Brandon, you got me hooked on ‑‑ I should ‑‑ that's actually a lie. I really like TypeScript but I'm not very good at using it, so I'm gonna, like, sort of use TypeScript here and it's probably gonna hurt anybody who is very serious about it. But what this lets me do is I've now created this repo. I can git push upstream and just set the upstream so we don't have to keep typing this whole thing out and I'm going ‑‑ did that create my ‑‑ or wait. Git remote. It didn't. So let me create an origin. Git remote, add origin. I want that to be git add GitHub.com. And we'll do a .git. I can up stream origin main. There it goes. If I click this, it's going to open up. There's our function all running. Ooh, yeah, that TypeScript episode was really good. Let's pull up ‑‑ we've got a couple of really good Typescript episodes so let's do some searching. I'm going to search for Typescript. We have this one, this one and this one. This is probably a really good starting point. Until recently was on the core TypeScript team. Zander, thank you so much for all the subs. I really, really appreciate it. Y'all are the best. Oh, and you subbed yourself. The best. So this is Orta teaching us about TypeScript fundamentals. This is Brandon ‑‑ or Ben, sorry, teaching us about the React in TypeScript which is a really good episode. And this is Tomasz teaching us about serverless functions. I'm not going to do too much with it today. I'm mostly just using it for the convenience of not having to write common js. I want to write EJS ‑‑ or ESM, I should say. So, then, what we're looking at. So scheduled functions use the Cron expression format. Okay. That's fine. But I would actually prefer that we run it like this. Okay. So first thing I need to do is I need to install Netlify functions. So NPM install. Netlify functions. Great. Should I deploy the site first? Let's deploy it so that way I can just deploy everything that happens and y'all can see it working. So I'm going to Netlify init. And that is going to create and configure a new site using the Netlify CLI for this. Choose my team. I'm going to say Netlify‑scheduled‑functions. Use the same name. My build command, there is not a site to build. Netlify functions. That's the folder we want to get. Why did it not? You know, I broke my GitHub off, like, my tokens are weird and I need to go figure out what I did. What we can do in the meantime, though, is I can go back to my sites and go here. Here's this and then I'm gonna go to my site settings. And we're gonna connect a git. So I'm just gonna reconnect here. This is the long way. If you like me have broken your token. Broken your token. Do a search. I'm in the wrong org so that's not gonna show me anything. Let's get this one instead. Try again. There it is. We're gonna do the main branch. We don't need any of that stuff so we can just get right into it. Deploy. There it goes. This is only a function. So it should happen pretty dang quick here. Oh, nice, always good. Ben Meyers wrote an Algolia bot for chat. What does that mean? Oh, I could play the game. Yeah, I forgot we have a game now. So you have the ability to do pattern matching while you wait. Here we go. This site is definitely deployed so I'm not gonna finish this game, but we can ‑‑ that's a fun thing to play if you want to do it. Then I have only a function deployed so I'm gonna go to Netlify/functions/hello‑world. What did I do wrong? I didn't make it a sync. Oh, silly, silly, silly. Okay. So let me git commit Netlify/functions/hello‑world. And we'll give it a message of fix. Make it async. And then when I push this, that'll kick off a new build. And this should go nice and quick because everything will be cached. Yep, there we go. It's gone. Functions are already done. And website. All right. So going back out here, that took, what, 18 seconds. Perfect. I'm happy about that. Then I can go over here. Run it again. And there is our ‑‑ okay, so that's the response we wanted. Good. So for local dev, though, we're going to work in Netlify dev, and that will give us a locally running instance here so that I can go to, again, Netlify/functions and we'll go to hello‑world. Tada. There is our function. Let me blow that up a bit so we can see what's going on. And then if I take this over to the left and this over to the right, and I change this out to save it, reload it. Okay. So, we're now doing local dev for our ‑‑ local dev for our serverless functions using Netlify Dev, which has I think got to be my favorite feature of the Netlify CLI. We can just use all this stuff and try it together, set up our redirects and all those good things. This, yeah, the Algolia thing that you're talking about sounds really dang cool. Stream Vite and drops a link to the stream Vite in the chat. Yep, I want that. That is very cool. Netlify should have acquired Wordle. I do love Wordle. Wordle's a good one. The game icons did change because originally in December it was more of a holiday theme. We switched over to a generic non‑holiday. Ben, please do ping me. That is very, very cool. So we've done what we needed to do to get this running, so now let's actually make it a Cron Job. No, that's not the one I wanted. I want this one? This one. Here it is. All right. So, I've install the Netlify Functions. Good. Now to make this a Cron Job, I can const schedule and ‑‑ oh, wait, I don't even need to do that anymore. I can do it like this. Schedule from Netlify Functions. And then what we do down here is we get our const of handler function and then I'm going to export const. I could probably do this differently. But we'll say handler and that's going to be schedule. And hourly would be good, I think. And then we just run our handler function. Okay. So, that is, in fact it. This function will run every hour on the hour, which is not fast enough for this stream. So I'm gonna make it kind of absurdly fast. So let's look at Cron syntax. Because I don't remember how to ‑‑ I never get this right. So let's look at the breakdown. We get minute, hour, day, month, week. And so if I want this to run every minute, I would go minute, hour, day, month, week. So that basically means run this once a minute. Which is a lot. You probably don't have many cases where you would actually want this to run every minute outside of a demo, but let's give it a shot and see ‑‑ what does the instructions say? So once we set this up, good. We have a test scheduled function. Oh, we can also do it in Netlify.toml. So we could say ‑‑ oh, this is cool. Okay. So the other way that we could have done this, if we didn't want to add this schedule wrapper, is we could make a Netlify.toml and then in here I could do functions.hello‑world and say schedule and we would run it hourly or, you know, daily or whatever. Good. Okay. So this is good. I'm happy about that. And using function language, developing debugging. Okay. Developing and debugging, this is what I was interested. So, today's scheduled functions only run on published deploys so deploy previews won't execute scheduled functions. That makes sense because as you're developing you don't want to fire off, like, a million of these and stuff. Similar to event triggered functions, functions that are published as a part of the deploy cannot be invoked directly with a URL. Interesting. I wonder if that's going to change here. So, here, it doesn't do anything anymore, right? It doesn't give me my outcome. It just kind of gives me a no op. That's the old one. This one. Okay. So that is running. So I wonder if it just kind of lets you execute to see that it works but it doesn't do it on the Cron Job. Netlify dev. Netlify functions, invoke my function. Okay. Let's try that. So I'm going to stop here. I'm going to run Netlify functions: invoke hello‑world. Ran into an error. Oh, yeah, okay, that would make sense. So you have to have two tabs open for this. So let me run Netlify Dev. And then I'm gonna run Netlify Functions: invoke hello‑world. I've actually never used this feature before. I'm in the wrong. Okay. Here we go. Learn With Jason scheduled‑functions and let's try that one more time. Yeah, there it is. And it gives us our outcome. That's dope. So that's really helpful because it ‑‑ I didn't even realize that that was a thing that we could do. I always have to, like, spin it up, up here. I'm using postman and stuff. Okay. That's pretty cool. I like that. I'm happy. So text yourself a positive encourage every minute. Oh, I love it. I love it. If I did it as, like, one star, wouldn't that just mean the first minute of every hour? Ooh, Crontab.Guru is a great resource. Let me pull that up so I'm not lying to everyone. So, Crontab.Guru lets us run at 405. If I did it at star ‑‑ at minute one, this would be every minute. Okay. But if we wanted to run it, like, every minute for hour one, so that would be, like, between 1:00 a.m. and 1:59 a.m., run every minute. That's every minute of every day. This would be, like, at minute zero. Hourly. And then, yeah. So there's a lot of ways to do that. But yes. It is ‑‑ it is, indeed, complicated, which is way I vastly prefer the hourly syntax, right? So that's ‑‑ that's the reason that these are so beneficial. Okay. So. Are there any others down here that hourly ‑‑ reboot is kind of cool because reboot would mean whenever the server restarts. That's not really applicable to us, I don't think, because it's a serverless function. But daily, weekly, monthly, annually and yearly mean the same thing. Okay. Yeah. Yup. Yup. Yup. Oh, you know what else we could do that? That's a good point. We could say, like, every 15 minutes, right? So every 15 minutes we could run it. Or every 10 minutes or every 30 minutes. So this is really helpful. But yes, lots of good ways to solve these sorts of things. And for our particular purposes, we're just gonna run it every single minute. It's gonna be pure chaos and it is gonna be wonderful. So we've got our Cron Job here. Let's see. We're developing and invoking. I think we can just push this. So let's echo Node_modules to my .gitignore. Do a check. I'm gonna git add everything. Good. Let's git commit and say a feature first Cron Job and I'm going to push. So let's go back out and we'll be building. There it goes. Oh, you know what I got to do, though? I got to enable Cron Jobs for the site. So let me go to functions and I need to click this button. We're enabling. I wonder if I'm gonna have to rebuild the site to get that to work. Let's see. I'm gonna guess that I do have to rebuild the site because I didn't have that checked. I'm gonna go back and trigger a deploy. We're just gonna manually trigger this one and say deploy the site. I'm curious if it's going to show us anything about whether or not we added a Cron Job. Hopefully it will. Let's see. Functions. Scheduling functions. Yeah. Look at that go. Okay. So, very exciting. And then what that should mean is if I go to this functions tab and this says "scheduled," then we can see that it was last executed ‑‑ I think it just showed ‑‑ did it show back here? Next execution today at 11:03 a.m. Okay. Let's watch. So we just got to kill a minute. Hey, it ran. So it ran at 11:02 and then it should run again at 11:03. And these are all run as ‑‑ yeah, it's all real time. There it is. There is our function invocation. Okay. We did it, y'all. We did a scheduled function, right? That's the whole thing. It did what we wanted. It was nice and fast and, you know, this is obviously 2.3 milliseconds is nice and quick. I think unless you're writing Rust ‑‑ now, granted, we didn't do very much here, but ‑‑ [ Music ]
Okay. So the next thing I want to try is I want to dig in here and see what we get if we try to do something with this, okay? So what I'm going to do is I'm going to go off screen and create a Notion thing that we can work with. So let's get ‑‑ I'm gonna create a new page. All right. So here is a Notion page. Oh, come on. Come back. There. Here's the new Notion page, and what I'm gonna do is I'm gonna make it into a table. All right. And so we'll call this "Cron Job Testing." So what I want to do is I want to go into the Notion API. And let's insert a new entry into this table every minute based on something. So I want to ‑‑ database object. Query a database. Let's query the database. List databases. Can I use that or is it just retrieve? Database ID is required. My database ID is this one? And, oh, the way you have to do this, too, is you have to, like, invite ‑‑ a thing here. So, actually, let me really quickly create an API key so that I have one that's unique to this. So, go to integrations. All right. So let me show you kind of what I'm doing here so that it's not fully mystery. I'm also trying to not, like, accidentally dox myself here. So I went to my settings up here. The settings and members. And then I'm going into integrations. You can develop your own integrations. When I click this button, what happens? Is it gonna show a bunch of things? No. So I'm gonna create a new integration. We're gonna call this "LWJ Scheduled Functions Testing." We're not gonna set any of that, but we are gonna set it to my own space. I want to be able to read, update, and insert content. I don't think I need any user information so let's leave that off. And then I'm gonna get this integration token. And the only way this is gonna work is if I share a table with this particular integration. So what I'm gonna do is pull this off screen, get my token, copy it, and put it somewhere that I can get to it later. Let's ‑‑ can I pull this out and put it over here? No, okay. Let's do one of these. All right.
You hackers. You dirty hackers.
JASON: I know, you hackers. Okay. So then I have that set up there. All right. So that's a ‑‑ oh, you know what I can do? Watch. Watch. I'm going to Netlify env: set and I'm going to say NOTION_INTEGRATION_TOKEN. And I'm going to set it into this key value and pull off screen for that. And then clear and just make sure I have the right version of the CLI here. I do. I ran Netlify env: list. And that gave me ‑‑
You hackers. You dirty hackers. (Laughter)
JASON: Yes. So that's all set. That's good to go. And we now have this available in our environment, which means that we can build something with it. So let's start by actually trying to, like, write a query. And so what I'm gonna do with this is let's go to my Notion here. I'm gonna ‑‑ wait, hold on. Hide this sidebar so I can come back. There we go. All right. So now I have this setup here, and what I think I need to do is get this ID. And then let's go back here and just try it. Like will that work? Does it execute if I try it like that? Why does it show me this if it doesn't ‑‑ oh, it's, like, filling this out for me instead of databases. Is this the right one? Okay. Don't entirely understand what's going on here, but let's try it and just see if I can get something to work. So I'm gonna install ‑‑ npm install @notionhq/client. And then I'm going to get this piece here. Let's bring this to the side. Oops. And we'll get over here. And let's just make a function that's, like, Notion table.ts. I'm gonna import handler. From Netlify functions and now we're actually gonna write a little real TypeScript here. So I'm gonna do export ‑‑ Oops. Const handler, and that's gonna be the handler type. And then that gets whatever it gets. But check this out. You can actually see the available types if I, like, go to event. Oh, it needs to be async. Jeez, I'm, like, really determined to make that mistake today. So I've got my event. And then in here, if I go to, like, event.I can see all the values. This is the benefit of TypeScript. By setting this handler type, it lets us know what's available in here. I don't know if we're going to use this right now, so I'm going to leave it out for the moment and instead I'm going to ‑‑ where did my Notion example go? Here it is. I'm gonna import client from NotionHQ Client. The auth is going to be process.env ‑‑ what did I call this? Notion integration token, right? So now that I have my notion integration token, I am going to set my database ID. DB ID and that's gonna equal. Where does this value come from? Like does anybody know ‑‑ database ID. What if I just want, like, the straight‑up ID? Who knows. Who knows. Copy link to view. Properties. Template. Learn about databases. Notion get database ID. Looks like people have Googled this before. If your URL looks like below, that is the database. Database ID and the V is the long ID. Much share your database with the integration. Okay, so let's do that part. I'm going to share this with scheduled functions testing. So that means that this now has access to it and I need ‑‑ so this is my database ID, I guess. So let's try that and see if I can just list all of these pieces. So to do that, I need to go back to the Notion docs and we're going to say const response = await notion.databases.retrieve database_id is going to be DBID. And let's just return statusCode of 200 and a body will be JSON stringify response. And we'll make that look a little bit nicer. And maybe to ‑‑ I think we'll get content type. Application JSON if we set this, it will maybe look a little nicer in certain browsers. So I'm going to save this. Go back here. I think this is already running, which means that if I go over here and I try Notion Table, API token is invalid. Okay. Hmm. Oh, did I restart since I had the ‑‑ let's make sure it pulls it in. There's my token and here's my database. Okay. So I just hadn't restarted to get that new environment variable. Okay. So we have the ability now to get this in, and we can see here that all of these are empty because we're not ‑‑ we're not actually using them. So let's delete this one. Delete this one. And then we'll just put one in. And I'm gonna make these really, really simple by actually just kind of deleting everything except for the name for now, and we'll add more later on. But let's run this one more time. And we get properties. Did it pull that data? Name, title, title, type. So this is just getting the database. Okay. So then I need to go in and get the actual database stuff. So let's query a database? Right? And let's get the pieces. So databases, database ID query, good. And, oh, you can do filters. That's really cool. Errors. And let's just look at the JavaScript. So I'm gonna do Notion databases query. Put the database query in. And then I can leave the filter out. Leave the sorts out. And it should just give us a response. So let's do databases query. It doesn't seem unhappy, so I'm gonna come out here and try this. Object list. Results. Here are our entries. Nice. Okay. This is great. So this gives us everything that we needed. I'm really, really happy about this. So we now have the ability to retrieve from a Notion database. Next, I want to insert from a Notion database, and then we'll start getting into the Cron Job stuff. So let me look here. And let's go back up to the top and say, update database. I want to update it. And to update it, I want to update with options. I don't really want that. I want to put something into the database, right? Because I don't really care about the hatch options. Hmm. Let's go keep digging here. We've got database object. Property object. Create a database. Update database. Update property schema. Pages. Hmm. Hmm. This might need us to go full screen here. So let's go into query sort, database object. Retrieve a database. Controls how it is updated. Okay. That's not what we want. Updates existing database. But what if I want to insert into the database? Users, common. Is it a page object? That seems wrong, right? Create a database. I don't want to create a database. All right. Let's maybe just ‑‑ Notion API insert database entry. Create a database. Getting started. Insert data in database via Notion API. Off to Stackoverflow we go. We're going to notion.pages.create and we set it as a parent ‑‑ ah, okay. So we can do this. Let's go in here, and I'm going to say Notion.pages.create. And then we need parent. And that's gonna be database ID. Okay. And then we also need properties. And the first one that we get, I think, is just name. Is that what I set? Just name. So we'll do name. And then we need to title. This feels like a lot. Text. Content. Like that. We'll skip the description. And I think that should do it. So let's try that. Okay. It says it worked. Moment of truth. Hey, we did it, y'all! Oh, boy. This is ‑‑ this is great. So now we have ability to do something on a Cron Job, which is what we wanted to do in the first place. So what should we do with our Cron Job here?
Holy buckets, did that just work?
JASON: What if we pull something from the dad joke API or something? Are these, like, actual clean dad jokes? I've never looked at this API. I don't want to do something that's, like, secretly uncle jokes. Okay. This is pretty good, actually. (Laughter) All right. So let's do this. Let's get a random joke as text and we'll just inject it into the joke. Football scores and commentary live would be good, but I don't know of any open APIs that do that. I'm gonna ‑‑ I'm gonna roll with this one for now. And depending on how quickly we get through this, because we've actually got a decent amount of time left, maybe we can try for something a little more ambitious as well here. So let's get into the dad joke. And so what I think we can do is we can say, const joke = await. I'm gonna have to import fetch, aren't I? Yes. So let's go with NPM install Node Fetch. Did y'all see that Fetch is landing in Node? There's a new ‑‑ where is it? Here. This is very exciting. You're going to be able to use ‑‑ is there a link to this, though? Oh, this one doesn't have a link. Where's the ‑‑ does this one have a link? I really just want a link to, yes, the GitHub issue. There we go. Merged. We have Fetch in Node now, which is very, very exciting. Which means that we'll be able to just use Fetch the way that we use Fetch and we won't have to do this whole dance of importing a poly fill to use it. So let's go here. And then we'll go here. And then I'm going to import Fetch from ‑‑ whoops. From Node‑Fetch. Might have to do a little bit of a dance here to make this work. I can't quite remember how the exports work, but let's try it. We're gonna say we want to fetch from ‑‑ not that. We want to fetch from dad jokes. Okay. And I want to send headers of ‑‑ oops. Accept. And we'll use text/plain so that we just get plain text. That's really all we need. And what we want to do then is we will get the response and we'll say, res.text. And then what should theoretically happen ‑‑ you know what? I was gonna check this and I've changed my mind. We're just going all in. We're just going to throw it straight in and see what happens. So I'm going to save, and just like that, yeet! Hey, first try!. I love it. Let's do it again. Get a different one. Did that one work or did that one fail? It said it worked. And there, it worked. Oh, dang it. Oh, that's cool. Okay. So, next thing we can do is let's use the Netlify toml this time. Let's go ‑‑ we'll go in here. And I'm going to say ‑‑ that one's already running on a schedule because we wrote it with code, so instead, let's do it like this. We will run Notion Table and we will schedule and this one was minutes, hours, days, months, years, right? So, that is gonna run every minute. We will run our Notion Table function. So assuming that works, I'm going to git add everything. Git commit. And say add a new dad joke every minute to Notion. Okay. Let's push that up. And make this a little bit bigger while we wait for our deploys. Every minute is gonna be aggressive, but I want to have a short time‑out. We'll actually extend that at the end to make it less intense. Because we're currently running two functions every minute, which is not what we actually want to do in any production environment, but for the sake of not making y'all stare at a screen while we wait for that to finish, we're gonna go faster. So we can see here that it is scheduled. And now let's head over to our functions. And we can see it's set up. It's scheduled to run every minute. And if we look at this table, what we should see ‑‑ there we go. There's one. And so every minute we should get another one. Oh, these are bad. These are really bad. You want to hear my favorite dad joke right now, chat? Why do chicken coops only have two doors? Because if they had four doors, they would be chicken sedans. So, did we ‑‑ what was the ID keyed off of? So the ID is ‑‑ the ID that we're using is this database ID here. And I have that stored in just a variable up here at the top. So that's ‑‑ that's what we're using right now. And the dad jokes don't need any APIs. It just gives you a random one. And we can see here another one came in. Charlie, all right, I'll give you back those subs. Sorry, everyone. (Laughter) Too dad, too furious. Love it. Love it. One joke per minute. Yup. Yup, yup,up. So this is great, right? We have been able to, with relatively low effort here, gotten this set up. Oh, no, Tony's doing one. Why aren't there two Yogi Bears? Because they made a boo‑boo. That one's bad. That's real bad. (Laughter)
Oh, jeez. Okay. Oh, here's another one coming in. So there we go. We've now made these every minute, right? So let's change this. And what I want to do instead is I want to run this daily. So I'm going to swap this out. And I want a new joke every day. That seems like a reasonable amount for me to get. And then I'm also gonna change this hello‑world to run once a day. But let's run it at noon every day. How about that? So this will run at the 12th hour every day. And, okay, git add everything. And then we'll git commit and say, you know, feature, make the Cron Jobs less aggressive. Okay. Oops. Git push. And up it goes. And that'll ‑‑ resisting arrest. (Laughter) That's a bad joke. Jeez. If a function is still scheduled, can you still call it manually? That is a great question. Let's try. I'm going to go to GitHub.‑‑ oh, no, what's the name of the site? I had it up here. Here. And I'm going to call Notion‑Table. No. So that's actually a good thing to call out. When you do a Cron Job, you are basically saying this is a system function not a public function. And they do call that out in the docs, if I can find them. Scheduled functions run on published deploys, but scheduled functions that are published as part of a deploy ‑‑ why can't I click? Scheduled functions that are published as a part of a deploy cannot be invoked directly with a URL. I actually think this makes sense, and here's why. Imagine you have a reporting or something that batch processes discounts or whatever it is, right, and you want that to run at a random interval or a set interval. If that is done manually, it could break a system, right? And so what this does is it still gives you the act to run this Netlify functions invoke, which will let you fire it, but it won't let somebody, like, spam the URL and make the system do things that it's not supposed to do. So, yeah, that is a ‑‑ that is a good thing ‑‑ it's a good thing to do. Yeah. Really, really excited about that. Ooh, and a D Doss is avoiding too. A lot of what I'm going to do be doing with my Cron Jobs, I'm imagining ‑‑ we track various metrics at Netlify for my team. I'm thinking I can go hit a few different APIs and put together, like, a little report for myself. I can insert it into Notion, for example. Or I could send it as an email using Send Grid or whatever. And that way I could get a daily digest or a weekly digest of things we're looking at so I don't forget to check those pages. Little things like that are so helpful. And a lot of times I don't do them because I don't want to go spin up a service. Then I got to remember that I used Zapier for this one. If I've got a suite of my utility things that I'm using, I'm happy about that. That feels good and useful. Since Simon not joined Netlify, will the real blitz stand up? Kind of. Somebody thought that blitz.js was my thing. I was like it's definitely not. I've used blitz.js for, like, I think one episode. We did ‑‑ and, like, Blitz is cool. So let me pull that up. Nope. Here's the search. Here's Blitz. If you are not familiar, Blitz is cool, however, I have nothing to do with it. It's just a coincidence. (Laughter) So, yeah. Probably need to do a follow‑up because I think Blitz has changed a lot. Think they did a hard fork ‑‑ not a hard fork, but they forked Next.js and they're working on a I think less dependent on Next.js model. So that it's more kind of drop in on any framework to be more batteries included. May not be a thing anymore. Interesting. Okay. Well, maybe I'm wrong. But yeah, lots of cool stuff going on in the space, and it's all happening so fast that I can't keep up with it. But this is ‑‑ like this is really exciting. So for y'all to go check check this out, just to recap what we did to get this working, is I went through this Netlify Scheduled Functions launch doc, and then I went to Netlify Labs. And enabled Scheduled Functions. So good things there. And then I went to my site. Went to the functions tab. Hit the "enable scheduled functions" button, and then in my function, we went about this two ways. So, we had a hello‑world where we pulled in this schedule helper from the Netlify Functions package and just wrapped it with a Cron Tab. And then there is also the way where we write a function as usual. So this is a standard serverless function. I'm using the TypeScript flavor. That let us do what we want to do. We're pulling a random choke from Icanhazdadjoke. That's something we can do on random demand. We set it up using this Netlify.toml syntax to add a daily new joke to the table. So this is a pretty, like, crash course‑y way of doing this. You can imagine anything you can do with a serverless function you can now do at an interval. This is really exciting because there are a lot of things I have to remember, oh, I got to push a button to start this report. I got to remember to go here to download this thing and put it in this channel so everybody can see it. I could use the Slack API to post a message once a day to my team that says, don't forget to, you know, monitor this thing or, you know, all sorts of really interesting things that I could do, even multi‑variant things. Like I can pull a report and I can email it and post it to Slack and post it to Notion all as part of the same daily job using a serverless function like this. So there are some really, really interesting possibilities here. Tony, yes, once you set a function as scheduled here, it is no longer available as a public function. So if we go back to our published site, which is here, right? Here's our published site. And I try to open this Notion table. It's gonna show up as access denied. And that is because we don't want people hitting our robot functions willy‑nilly. So by would need to explicitly double publish that function so that there was a bot version and then a public access version, if you want to do that. No, Tony, this is all very new, so I'm kind of repeating it to remind myself as well. Okay. Chat, we've got call it 20 minutes. What do you want to try? What questions do you have? What are you interested in seeing from these scheduled functions? I'm gonna dance while you think. How to display jokes on the site. (Laughter)
Chris, if you dance now, you're dancing with me. Okay. So displaying ‑‑ okay. If we want to build a static site. Yeah, let's do it. Let me go here. And then what I'm gonna do is let's go up here. We'll go with an index.html. This will work. This will be fine. So I'm gonna close Netlify Functions here. I'm gonna use an emit abbreviation. I don't want to really pull from the Notion API because I'll need to stand up a full page or something like that. I'm probably gonna skip that part, but maybe we could do ‑‑ I guess I could do a Vite project, right? Why don't we just do that? It's really, really fast to use Vite. So let's npm init vite at latest. We'll put it in site. I'm gonna use ‑‑ I'll use React because I know that. And then I'm gonna go into the site. We'll run an npm install. And inside of here, we will be able to call our function. So what we can do is actually duplicate this one and let's say, like, git notion data and we can revert to what we did before, which is to get a notion databases.query, right? And then in here, we're just gonna ‑‑ get out of my way. Cheebas. Go down here. Okay. So now we've got what should fetch all of the data from our API. Let me run a ‑‑ let's go back. So what I'm gonna do is run a Netlify ‑‑ is that the base ‑‑ for build. So we'll set the base to site. Is that gonna break everything? That probably is gonna break everything. So what I'm gonna do instead is move everything out of here. Which means I need to merge some stuff. So let me just get this and this and these. Get all these. Get these. Copy. Go out here. How fast can I spin up a whole site? Let's find out. So I'm gonna go here. Here. And here. And then I need to turn these into dev dependencies. Okay. This needs to be fixed. Good. All right. So that is fine. I can leave that. And then I'm gonna take this git ignore. Do I need all of these? Yeah, why not. Let's get these. And put that in there. Then we've got all of these bits that I'm gonna move out here. Yeah, we can replace that index.html. All right. Good. So then I can delete this. And I can npm install. Good. Then I'm gonna run Netlify Dev. And let's see what happens. Should auto detect that we're using Vite. Okay. So now we're running Vite. And what I can do then is I can ‑‑ let me go back to this table, and I'm going to run ‑‑ what do we call this function? Notion ‑‑ get notion data. Okay. So here is our list. Right? And then what we can do in here, if I go back to my index.html, that's gonna pull in app.js, right? Yes. Let's go in here to app.jsx. That pulls everything. Good. So then I'm gonna get a useEffect. And we can drop this part out so we'll instead say items, I guess, and set items. That'll be a useState with an empty array to start. Okay. And then we're gonna useEffect. Inside of here, we will have that run on ‑‑ I guess on ‑‑ just on load? Yeah. Because that's fine. Then we're going to set up an async function to load Notion data. That is going to const response = await fetch. And we'll go to Netlify. I'm gonna have to make this functions/get‑notion‑data. I'm gonna have to make this an absolute URL because Fetch doesn't like it if you don't. We'll go local host 8888 to start. Have to figure out how to make that work. And then I'm going to then res.json. That should give us data. Then what I want to do, that's gonna give us a response. Let's console.log that to start. That will give us a starting point for getting our data. And then down here, we can do something like let's get ‑‑ let's actually get rid of all of this. And instead, we'll put in an h1 that says, like, data loaded from Notion. Published every minute, right? Because we're gonna have to go back down to an every minute publish to make that work. And then what we're gonna do is we are going to set up a unordered list and we'll go with items.‑‑ no, items.map item and for each of these, we're gonna return a list item with a key. That's gonna have to have some value. And then the name, which will be item.name, theoretically, but we're probably gonna have to do some mapping here. So let's actually just assume we're gonna have an item.name and an item.key, and that's what we're gonna display. So I need to figure out how to get these details in here. And now we need to loadNotionData. Great. Okay. So, how have we done? Let's go back to the homepage. There we go. We've got some data loaded from Notion. We've got an object. That's gonna give us results. So I am going to ‑‑ let's go with const loadedItems is gonna be response.results.map. And then each of these I'm going to turn into a simplified object that just has a key and a name. So we've got an ID. I'm gonna make that my key. So I can actually just simplify this even further. Let's do it like this. So that we just return this object right away. So the key is going to be res.ID and the name is going to be res.properties.name.title. Oh, boy. Yikes. Okay. This is gonna be messy, so I'm just gonna let it go and be gross. Because I don't want to deal with a lot of type checking. Or, like, presence checking. So we're just gonna let it be bad. So we're gonna go res.‑‑ go straight into properties. Res.properties.name and then we're gonna get .title. Oof. Then we get right on in here and we say plain_text. And that should be okay. This is a bad joke. Okay. So, theoretically speaking, that is what we want. And then I can set items to loadedItems, and that will theoretically speaking give us a list on the page. Let's give it a try. I broke it. Why didn't this work? What went wrong? Are we getting ‑‑ oh, they're down here. Okay. So, this header is weird. Let's simplify that then and get rid of this header so that we don't have the styles working against us here. All right. So now we have this. Not the best looking thing that we've ever made, but it also means that whenever new data gets added, this will get updated. And if we were gonna do this with, like, a static site generator, Next.js would be get static props. Gatsby, we could load it in with a data piece. 11ty. I set this up with Vite because I knew it would be fast and we're short on time. Would it be better to use the map with use effect inside the function? Maybe. Yeah, we definitely could. Like it's not a bad idea. Yeah, why don't we do that? Let's do it. We'll take this out here to the get notion data. And what we're gonna get on this response is gonna be here. Right? And then ‑‑ properties does not exist on type ‑‑ what are you yelling about? Object page ID. That's not true, though. What does it think exists here? ID object.‑‑ no, okay. We're just gonna maybe stop helping. How do I make it ignore? Look away, TypeScript friends. Okay. So, we're gonna do that. Loaded items. And then let's try this one more time over here. We're gonna do a get notion data. And there are a vastly simplified set of data. So that's a good call. Now we're not sending too much back to the browser. We're not sending stuff that's not gonna be used. That means in our app.jsx, instead of this, we can do this. Okay. Now this should all work as expected. It does. Good. Good. Good. Let's deploy it. So, going to git commit everything. Let's add everything and just make sure I'm not adding a bunch of junk. I'm not git commit. Build a site using the Notion data. Okay. I'm gonna git push. Here's where things are going to get really interesting. What I want to do next. I want to rebuild that site on a regular basis. Down here we're gonna find build hooks. Great. I'm gonna add a build hook. And I'm gonna say rebuild triggered via Cron Job, and I'm going to save. All right. Then I'm going to copy this. And out here, we're gonna make a new function. And this function is going to be called rebuild.ts. Okay. And we're gonna import handler from Netlify functions. I'm gonna import fetch from Node‑fetch and then I'm going to ‑‑ oops. Export const handler. Which is a handler. And that is going to be an async function that will const response = await fetch. And we'll just hit our deploy ‑‑ you know what I can actually do? We're just gonna await and then I'm gonna return status code 200. Body of rebuild triggered. Right? And now we have a function that is Cron Jobable. Oops. Maybe not that many. Maybe that's too many. And let's set up a rebuild. And for now, let's run it every minute. Okay. And so I'm going to git add all. Good. Git commit. Actually, let's test this real quick and make sure that I actually made it work before we ship it. Okay. So here's our function. And if I go to Netlify functions rebuild, it says rebuild triggered. If we go out here and actually look, did it do it? It did not. Okay. Why ‑‑ oh, because it has to be a post. That makes more sense. Okay. So I need to go into to go back in here. And we're going to change this to be a method of post. All right. Let's try that again. Here. And then I'm going to ‑‑ where was it? Where was it ‑‑ there it is. We have a rebuild happening via a post. So I can git add all. Git commit. Add a site rebuild with Cron Job. Let's push. And this is gonna go live momentarily. All right. There are a lot of things that are going on here that kind of, like, not as polished as you would want them to be. For example, I am using a client‑side Fetch so it doesn't really matter that we're rebuilding the site. It's not gonna change anything. However, if we were publishing this live, which it is now live. You can go and see it here. Are you gonna pretend like you don't work? Line type of octet stream. What the heck does that mean? Main.jsx, are you not publishing? Uh‑oh. Oh, you know what it is? It's because I didn't ‑‑ I didn't change out the, like, build commands and stuff. So when you run an npm run build, it is going to create Vite build and then it sets things up in Dist. I need this to be npm run build and I need this to be Dist. Okay. Save. Back we go. See our Cron Job is already working. But this one I'm gonna cancel because we know it's gonna fail because we hadn't updated those pieces yet. So I'm going to go back. Trigger. And this one should actually deploy the site because now the details are correct because we ‑‑ oh, I didn't change the URL I'm fetching for data. That is gonna break as well. Dang it. Okay. So that is gonna live here. Yes. Okay. So let's take that. Put that in there. Okay. Okay. So we fixed that URL. And now we've got a site that ‑‑ so this one's built. If I reload the page, we should get ‑‑ it should load, but not load the data because we're hitting local host. But once this next deploy works, which will happen momentarily, now it's actually building. Good. It's deploying the site. Good. And there we are. We now have a site that is pulling from the Notion database. Yeah, Kenny, I think the challenge with Fetch is that Fetch yells at me if it's not an absolute URL, I think. Maybe that's only in Node, actually. That's a good question. And one that's probably worth digging into. I don't have time to do it today because we are out of time, but check it out. The deploy hook is now working. And we're seeing Cron Jobs fired every minute. So the site will now be rebuilt every minute. I'm gonna turn that off because obviously we don't actually want to rebuild the site every minute, but one way that this could be really useful is imagine you've got an API. Now, that API is going to be serving up assets. Those assets don't change often. But they do change often enough that you want to make sure they're refreshed. You can use an on‑demand builder to make sure that those functions don't ‑‑ that they return like cached API responses. But then you can set up a Cron Job to redeploy which would clear those API responses every 15 minutes, every hour, every day, whatever it is to get the latest data, and that way you would have a really, really performant API endpoint that also stays up to date because it refreshes at whatever the interval is that keeps your data up to date. So there are some really interesting patterns and ways that you can approach these sorts of things that it would really, like, solve a lot of problems. And yes, Jordan, to answer your question, if we get out here and try to manually trigger, let's go to Netlify/functions/rebuild. I'm gonna get access denied because you cannot manually trigger a scheduled function. They are protected in that way. And that's for a reason. Because, yeah, if I had this out there and it was public, you could just spam my site and use up all my resources and cause a bunch of problems. Some DDoS issues. This eliminates that issue by making sure you have to intentionally make this available for me to go and do, which I wouldn't recommend. Don't do that. So that, I think, is gonna be a successful run, chat. I really hope that y'all are excited about this. I think there's so much cool stuff coming so that you could go and try this out. I'm really curious to see what you build. So as you're building out your own Cron Jobs, please tweet at us. Tweet at me. I'm ‑‑ I think you all probably know my Twitter, but just in case, here is me on Twitter. And send me what you're working on. I want to hear about it. You can also tweet @Netlify. Make sure you hit up Simon, because this is a very cool feature and he is very, very excited. We've got docs here if you want to get into the docs. Very, very good things coming out of this. I'm just so dang excited. Make sure you keep an eye on Netlify Labs, too. We have so many fun things coming up. This is ‑‑ this is the first of a decent number of pretty exciting launches that we're doing. I am so, so stoked about our engineering team right now. They're doing so much good work. Our product team is killing it. So keep your eyes peeled. Lots of good stuff coming on the Netlify front and I'm really excited to see what you build with it. So let's call this one a win. Thank you all so much. Who should we raid, chat? You got anybody on your mind that you want to go and raid? Let's see. I see ‑‑ oh, you know who's live right now? Michael jolly who has not been back for a while. I'm going to go ahead and send us over to go see Michael. So let's do that. Y'all thank you so much for hanging out. While we are waiting for that to happen, make sure you check out ‑‑ we've had White Coat Captioning here with us. Jordan's been taking things down all day. Thank you very much for that. That's made possible through the support of our sponsors, Netlify, NX and Backlight. Please check them out. While you're checking out things on the site, make sure you check out the schedule. We've got a ton of really, really good stuff coming up. Make sure you go and hit that schedule. Add it on Google calendar. Lots of fun things coming down the pipeline. With that, we'll see you next time, chat. Thank you all so much for hanging out.
Learn With Jason is made possible by our sponsors: