[{"data":1,"prerenderedAt":439},["ShallowReactive",2],{"footer-primary":3,"footer-secondary":93,"footer-description":119,"100-apps-100-hours-ai-app":121,"100-apps-100-hours-ai-app-next":171,"sales-reps":187},{"items":4},[5,29,49,69],{"id":6,"title":7,"url":8,"page":8,"children":9},"522e608a-77b0-4333-820d-d4f44be2ade1","Solutions",null,[10,15,20,25],{"id":11,"title":12,"url":8,"page":13},"fcafe85a-a798-4710-9e7a-776fe413aae5","Headless CMS",{"permalink":14},"/solutions/headless-cms",{"id":16,"title":17,"url":8,"page":18},"79972923-93cf-4777-9e32-5c9b0315fc10","Backend-as-a-Service",{"permalink":19},"/solutions/backend-as-a-service",{"id":21,"title":22,"url":8,"page":23},"0fa8d0c1-7b64-4f6f-939d-d7fdb99fc407","Product Information",{"permalink":24},"/solutions/product-information-management",{"id":26,"title":27,"url":28,"page":8},"63946d54-6052-4780-8ff4-91f5a9931dcc","100+ Things to Build","https://directus.io/blog/100-tools-apps-and-platforms-you-can-build-with-directus",{"id":30,"title":31,"url":8,"page":8,"children":32},"8ab4f9b1-f3e2-44d6-919b-011d91fe072f","Resources",[33,37,41,45],{"id":34,"title":35,"url":36,"page":8},"f951fb84-8777-4b84-9e91-996fe9d25483","Documentation","https://docs.directus.io",{"id":38,"title":39,"url":40,"page":8},"366febc7-a538-4c08-a326-e6204957f1e3","Guides","https://docs.directus.io/guides/",{"id":42,"title":43,"url":44,"page":8},"aeb9128e-1c5f-417f-863c-2449416433cd","Community","https://directus.chat",{"id":46,"title":47,"url":48,"page":8},"da1c2ed8-0a77-49b0-a903-49c56cb07de5","Release Notes","https://github.com/directus/directus/releases",{"id":50,"title":51,"url":8,"page":8,"children":52},"d61fae8c-7502-494a-822f-19ecff3d0256","Support",[53,57,61,65],{"id":54,"title":55,"url":56,"page":8},"8c43c781-7ebd-475f-a931-747e293c0a88","Issue Tracker","https://github.com/directus/directus/issues",{"id":58,"title":59,"url":60,"page":8},"d77bb78e-cf7b-4e01-932a-514414ba49d3","Feature Requests","https://github.com/directus/directus/discussions?discussions_q=is:open+sort:top",{"id":62,"title":63,"url":64,"page":8},"4346be2b-2c53-476e-b53b-becacec626a6","Community Chat","https://discord.com/channels/725371605378924594/741317677397704757",{"id":66,"title":67,"url":68,"page":8},"26c115d2-49f7-4edc-935e-d37d427fb89d","Cloud Dashboard","https://directus.cloud",{"id":70,"title":71,"url":8,"page":8,"children":72},"49141403-4f20-44ac-8453-25ace1265812","Organization",[73,78,84,88],{"id":74,"title":75,"url":76,"page":77},"1f36ea92-8a5e-47c8-914c-9822a8b9538a","About","/about",{"permalink":76},{"id":79,"title":80,"url":81,"page":82},"b84bf525-5471-4b14-a93c-225f6c386005","Careers","#",{"permalink":83},"/careers",{"id":85,"title":86,"url":87,"page":8},"86aabc3a-433d-434b-9efa-ad1d34be0a34","Brand Assets","https://drive.google.com/drive/folders/1lBOTba4RaA5ikqOn8Ewo4RYzD0XcymG9?usp=sharing",{"id":89,"title":90,"url":8,"page":91},"8d2fa1e3-198e-4405-81e1-2ceb858bc237","Contact",{"permalink":92},"/contact",{"items":94},[95,101,107,113],{"id":96,"title":97,"url":8,"page":98,"children":100},"8a1b7bfa-429d-4ffc-a650-2a5fdcf356da","Cloud Policies",{"permalink":99},"/cloud-policies",[],{"id":102,"title":103,"url":81,"page":104,"children":106},"bea848ef-828f-4306-8017-6b00ec5d4a0c","License",{"permalink":105},"/bsl",[],{"id":108,"title":109,"url":81,"page":110,"children":112},"4e914f47-4bee-42b7-b445-3119ee4196ef","Terms",{"permalink":111},"/terms",[],{"id":114,"title":115,"url":81,"page":116,"children":118},"ea69eda6-d317-4981-8421-fcabb1826bfd","Privacy",{"permalink":117},"/privacy",[],{"description":120},"\u003Cp>A composable backend to build your Headless CMS, BaaS, and more.&nbsp;\u003C/p>",{"id":122,"slug":123,"vimeo_id":124,"description":125,"tile":126,"length":127,"resources":8,"people":128,"episode_number":132,"published":133,"title":134,"video_transcript_html":135,"video_transcript_text":136,"content":8,"status":137,"episode_people":138,"recommendations":152,"season":153,"seo":8},"0aae287d-3916-4d91-9310-25828998e562","ai-app","896095989","What if you could build an app that builds itself? That's the question Bryant seeks to answer in one hour on this AI themed episode. Follow along as he builds a custom Directus extension that connects with the OpenAI GPT-4 API and updates the projects underlying data model based on a simple prompt.","3ca1cb87-d47c-477a-af2f-a64dfe1bbd04",66,[129],{"name":130,"url":131},"Bryant Gillespie","https://directus.io/team/bryant-gillespie",6,"2024-01-15","Mission: AI App Generator","\u003Cp>Speaker 0: Hi guys. Welcome back to another episode of 100 apps 100 hours. I'm your host Brian Gillespie, developer advocate at Directus, and today we are tackling the elephant in the room, AI. So we're gonna be building an AI app generator. By that, I mean an app that can update itself with new data models and new interesting ways to work.\u003C/p>\u003Cp>So what's the inspiration for this? So I've been, over the last couple weeks, I've been seeing all these videos on X or Twitter, whatever it is, that show, this application TL draw where you can go in and sketch something out inside this quick little application, click a button, and it will use the open AI vision API and some of their other tools, I guess, to actually generate real code that makes whatever you sketch come to life. So really excited by that. We're gonna try to replicate something similar here. I'm calling this the mighty morphing App Ranger because I grew up with Power Rangers.\u003C/p>\u003Cp>It was one of the I think it was one of my first Halloweens. So if you're new to the series, let's dive in. There are 2 rules. We have 60 minutes to plan and build this application, no more no less, and the second rule is just use whatever you have at your disposal. So in this AI episode it should be pretty interesting to see how far we can get with this.\u003C/p>\u003Cp>One of the ways that I wanna start here is just by charting things out, and we'll try to upload that chart to chat gpt or open AI and and spit something back out that we can use. So without further ado, let's dive in and start building. So like I said, I like to plan everything first. So let's just draw some stuff on our artboard here and kind of sketch out what we want this actual application to do. So the very beginning we're gonna draw a diagram of diagram diagram of an app.\u003C/p>\u003Cp>Upload, we want to, what, send that to the application. Let's shrink this down. That's really large. Alright, you can tell even though I use Figma quite a bit it's still not my strong suit. So we'll draw a diagram of an app.\u003C/p>\u003Cp>We will send, use OpenAI Vision to create a data model based on the diagram, and then we're going to update our app based on that code or data model automatically. So we want the app to automatically update itself, but before we dive into that, let's just try to proof the concept out. Right? So let's draw an app. Let's start with something simple like a CRM.\u003C/p>\u003Cp>Here's our CRM app. What are the different data models or the different tables, the the different collections as we call them inside Directus, which is what we're gonna be using for our back end and what we're gonna build our application with. So we've got contacts. Well, we probably got some organizations. We've got a relationship between those.\u003C/p>\u003Cp>We probably have some deals, and we can use some arrows for the relationships. And if we just look at contacts and orgs, we probably have maybe like a junction table or something here. I don't think it left enough room, but let's call this, what? Organization contacts. Give that a little more room.\u003C/p>\u003Cp>Okay. Alright. So there's our basic diagram. Maybe we go in and inspect some fields out for this as well. We got a first name, last name, email, phone, title.\u003C/p>\u003Cp>K. We've got a name for the organization. We've probably got an address. We've got, what, what else is on the address? I got lost on my Figma file here.\u003C/p>\u003Cp>So address, name, we got some contacts. We probably got organizations that those contacts belong to. That's probably good enough. For the deal, we probably have a deal name, deal value. And what else?\u003C/p>\u003Cp>Close date maybe? Alright. So that is a pretty simple CRM app. Let's make our diagram pretty. And now let's just take a screenshot of this and let's just proof this concept out.\u003C/p>\u003Cp>We can use just the regular chat GPT for this because it it has that underlying vision API that we're gonna use once we start building our app. But I can upload this into chat GPT and say something like this. You are a SQL, post grace, and direct us master. Please create a SQL query, SQL query that will create the data model for the this CRM application as designed in the in the what, in the diagram attached. Please fill in and make the data model more robust.\u003C/p>\u003Cp>So, honestly, I'm I'm just curious to see if this will actually work to begin with. What happens? Oh, no. Did I lose all of that? Did that really happen?\u003C/p>\u003Cp>Open this up. Oh, no. Okay. Well, that's a hazards of the job. Right?\u003C/p>\u003Cp>Restore recently closed. Please, you are a post grace and directus master. No hidden camera tricks on this show. Just fun here. Sometimes you fat finger one of these.\u003C/p>\u003Cp>Please create a SQL statement SQL query that will create the data model for our app as in the diagram. No. The conversation is not helpful so far. Let's do a new chat. Restore recently closed.\u003C/p>\u003Cp>Alright. Sometimes the technology doesn't work out so well. Please create a SQL query for Postgres, that's the database we're using, that will create an app create the data model for an app based on the diagram. Alright. I'm gonna copy this just so I don't have to what in the world is going on?\u003C/p>\u003Cp>Alright. Let's just try this again. Holy moly. Wow. Okay.\u003C/p>\u003Cp>So we've eaten up a lot of time there just messing with chat gpt. That's a lot of fun, but let's see what this comes up with. Looks like it has to create a SQL data model, SQL code. So if we just look at this, we've got a contacts table. That's good.\u003C/p>\u003Cp>We've got the organizations table. We've got a associative table. That's a junction table. We have a table for our deals, and then we have some kind of statement that is altering the table. Okay.\u003C/p>\u003Cp>Please rewrite to use UUID to add a primary key of ID for all the tables and use UIDs. Just want to make sure we all have a primary key of for each one. Alright. Cool. Okay.\u003C/p>\u003Cp>So now we've got the IDs. We're using the UUID instead of just an integer there. Alright. Great. Okay.\u003C/p>\u003Cp>So looking at this, we can test it out. Right? How are we gonna test this out? So, what I've got set up, for this app is basically a Docker Compose file that I'm using to generate my back end, which is Directus. And if I pull up my Directus instance and just log in, so we go to admin atexample.com.\u003C/p>\u003Cp>We do password. We'll log in and if I zoom in just a little bit you can see this is a completely blank app. Nothing here. So what Directus does behind the scenes, it sits alongside your Postgres database and it will introspect that database, which basically means anything that any changes that you make inside that database or any SQL database, doesn't have to just be Postgres, it will mirror those changes in real time. Sounds great.\u003C/p>\u003Cp>How does it work in application? Alright. So we go in, let's just pull up this database inside TablePlus. Right? So I could see all of my directus tables here.\u003C/p>\u003Cp>There's no other tables. We've got some items in here for, like, post GIS, just so we can do geo data inside here. But let's just copy and paste this inside the application, or inside table plus, and we're gonna run this SQL query. The only thing that I'm gonna do here is just change this application. I'm gonna change the organization contacts, And what I'm gonna do here, I'm just gonna change this so that there is a ID and that's the primary key instead of a composite key.\u003C/p>\u003Cp>But other than that, let's just run this thing. So hit run all. Okay. Altered table has been altered. If I refresh, we can see that we have created those different tables and, you know, the associated columns.\u003C/p>\u003Cp>Great. Now if I load up Directus, I can see that Directus, just refreshing the screen, has recognized that those tables are inside our database. So I can go through and click to configure these tables. We can see that my different fields are coming through, and if we click on those we can configure what the actual UI would look like for these things. So if I just go through and click on a few of them, maybe we want to hide this specific field.\u003C/p>\u003Cp>Directus really easily allows you to update this data and the presentation of it. So basically now we're just controlling the form that displays when we add a new contact. So let's just edit that a little bit, put the title down there. Great. Yeah.\u003C/p>\u003Cp>So if we go in, we refresh, we've got our contacts, and now I could go in and add my contact. Great. 555-555555. Looking great. Brian@directus dotio, and I'm a developer advocate.\u003C/p>\u003Cp>Cool. So now if I go into table plus here, we can also see there's my record. Right? So Directus is really nice in that it mirrors everything, but it also gives me a REST based API. So I could go in and any of those collections, if I were to, just give read access to just the general public, we could also create different user roles here.\u003C/p>\u003Cp>But if I were to go in and just copy my address I could do something like this where do items slash contacts and boom I get it ready to use REST or GraphQL API that we can use to build our back end with. That's great, this is really cool. How can we take this further, right, how can we make this application able to adjust itself, right, what does that actually look like? So Directus has built in automation, using flows. So flows you could set up, any type of simple or complex automation for your data.\u003C/p>\u003Cp>You can make third party API calls, receive incoming webhooks, all of those great things. And I'm thinking we can use a combination of flows and chat GPT or the OpenAI API, that's a mouthful, to actually be able to send this a prompt, have it update the data model automatically. Now there are multiple ways to adjust the schema or your data model of a Directus application. So if we look at the documentation, Directus actually has some schema endpoints where you can go and snapshot your data model. So you make a request, your schema Let's see if we see the sample response.\u003C/p>\u003Cp>This is the diff, so it actually takes a snapshot of the existing schema and then compares that, with what you've got and comes up with a difference. But it's basically a bunch of JSON data that represents the data model inside our database, inside our direct instance. It is a fairly specific and complex syntax, though. So I'm not sure about OpenAI being able to actually parse this. You know, I'm really concerned about the accuracy for this because it's kind of a a niche syntax that it has to adhere to.\u003C/p>\u003Cp>Right? So maybe we could go the SQL route because it seems like, OpenAI understands SQL pretty well. Just just guessing. I'm not sure if it does or not. I guess we'll find out.\u003C/p>\u003Cp>Right? So how can we actually do this? Direct is it doesn't allow you to run raw SQL, rightfully so. Easy way to blow up your database. But I think that's how we're gonna have to do this.\u003C/p>\u003Cp>So what can we do? Let's reach for a custom extension. So I'm just gonna go to the docs. We're going to create an extension, and so let's pull up our application. I'll just open this up.\u003C/p>\u003Cp>Alright, so we've got the npx command. Yes, We will create a Directus extension. And there are a couple different types of Directus extensions that you can create. I say a couple, I mean there's a lot of Directus Extensions, but what we're going to do is create an endpoint that we can call and run that actual with a SQL query provided by OpenAI and run that actual query. So first off foremost, Nat, maybe we can get, like, a big disclaimer on here.\u003C/p>\u003Cp>Do not do this sort of thing in production. Our engineering and security team will probably both be mad at me for this, but we're going to do the custom endpoint extension. What are we gonna call this? Let's just call it raw SQL is the name of our extension. We can use typescript, that's fine.\u003C/p>\u003Cp>And boom. So Directus will go through and scaffold out this extension. I could see it here inside my raw SQL. Cool. Alright.\u003C/p>\u003Cp>So now what I wanna do, I'm just gonna drag and drop this into our extensions endpoints folder directory and I'm gonna do one other thing here as well. When we actually build this extension, I'm just gonna drop it into the root of that raw SQL directory, instead of a distribution or dist directory, because it has to be inside the root directory for Directus to pick up on that. Alright. So now we're gonna go to extensions slash what endpoints/rawsequel. Okay.\u003C/p>\u003Cp>We're gonna do npm run dev, or it says npm run dev. So let's run that. And one of the other things that I've got, if you are working locally with Directus, you're building extensions, probably one of the things that you wanna do inside your config is set up this extensions auto reload, which basically means anytime that you rebuild that extension, which, you know, we're doing here, it will reload that without having to restart the direct assistance for you, which is nice when you are building. Alright. So if we take a look at our existing extension here, we hit save.\u003C/p>\u003Cp>It's gonna build that endpoint for us. And looks like Directus is reloading extensions. So now if I go to our Directus URL, local host 8055, and I do raw SQL, we get this hello world. Right? Great.\u003C/p>\u003Cp>But what do we want to actually do now? We want to actually run a a raw SQL query. So how do we actually make that work? How do we get that done? Underneath the hood, Directus is currently using a query builder called connects or nex or that.\u003C/p>\u003Cp>I never know how to say this thing, if you pronounce the k or not. But it does have a raw it has a raw query object or a raw method that you can call to actually run a raw statement against your database. Again, giant disclaimer, the lawyers say don't do this in production because it is super easy to blow up your database, and also don't trust AI fully to build your application for you. Lots of red flags on this one, but should be interesting nonetheless. Right?\u003C/p>\u003Cp>So what are we gonna do? We are going to look for our custom endpoints, and Directus gives us access to that underlying database instance. So the endpoint receives the router and the context. So if we do something like this, we've got our context, if I can actually type, and we're gonna do let's make this a post route and where we let's just call it raw SQL run. Alright.\u003C/p>\u003Cp>Cool. So we'll wrap this in a, like, a try catch. And what are we gonna do? We are going to pick up the query from the body. Yeah.\u003C/p>\u003Cp>Okay. And we are going to do what? We're going to do something like this where we have context dot databasecontext.database.raw, and then we're gonna run that query, and then we are going to return the result. Alright. Let's do our catch error, error dot message.\u003C/p>\u003Cp>Now let's see what happens if I refresh. Raw SQL, nothing happens on this, but let's go to run. Raw SQL does not exist. That's that's great. We want it to not work when we run that, but let's just open up something like Postman, so we could quickly test this.\u003C/p>\u003Cp>I I'm not really a curl guy. I always struggle with the command line. Alright. So we will pop this in. We've got raw SQL run, and we should be passing what?\u003C/p>\u003Cp>A raw JSON. K. And we've got our query. And let's just say select from what contacts. Alright.\u003C/p>\u003Cp>We hit posts. Raw SQL run does not exist. Why is that? Rawsequel/run. Why do we not exist?\u003C/p>\u003Cp>Great question. We are 36 minutes in. Raw SQL run does not exist. Alright. Let's see.\u003C/p>\u003Cp>We're getting a wait can't oh, the, this needs to be what? Async. Okay. Boom. Alright.\u003C/p>\u003Cp>So we just ran a raw database query. So you could see that query here, and it is returning the result from that query. Alright. Great. One of the other things that we could do here just to make this a little more robust would be to use the accountability object that gets included inside our services here.\u003C/p>\u003Cp>Where is this at? So each request we could take a look at the accountability. So we do something like this where only an admin user could do this. So if rec_.accountability ability dotuser.isisadmin? Is that what it is?\u003C/p>\u003Cp>I think that's it. Only admins can run raw SQL queries. Did I spell that right? Req.accountability. Let's see.\u003C/p>\u003Cp>Only admins can run raw SQL queries, so now we have to be logged in to actually run that. And, cool. So now if I go to our admin user and just generate an access token, and if I pass that in our authentication here as a bearer token. Okay. Cool.\u003C/p>\u003Cp>So we've added a little bit of auth to this endpoint to, you know, run our our SQL queries against the production database. Great. Alright. So now we've got our application here, and I refresh. I could see we've got contacts, deals, organizations.\u003C/p>\u003Cp>How we doing on time? Let's go in and let's add a new collection. We're just gonna call it, what, app, transformations, mighty morphin' power rangers. You know, we could probably even call it, like, AI migrations or something like that. Right?\u003C/p>\u003Cp>We'll generate a type for it. Do we really need these extra fields? Maybe we can do that. Alright. Alright.\u003C/p>\u003Cp>So let's give a let's have a here's the prompt. And I'm trying to think of ways that we could make this more robust. Let's see the SQL. Let's call this a query, and let's add another field. Maybe we could ask OpenAI to give us a way to undo.\u003C/p>\u003Cp>Okay. Alright. So now if we do this, Yeah. Maybe we should have had that status field as well. Has it been applied or not?\u003C/p>\u003Cp>So let's just do a drop down. So status, we'll do applied, unapplied, unapplied. Okay. And then any default values here will be unapplied. Cool.\u003C/p>\u003Cp>Alright. Great. Okay. So now we've got our AI migrations. You know, I I could go with the chart thing, but we could also make this fun where anybody can create new data for those or new applications.\u003C/p>\u003Cp>So let's do let's start by building our flow first. Alright. So flow flows are how we automate data inside Directus. So let's create a new flow, and we'll say call open AI, and we could trigger this a couple of different ways. We could trigger it manually or we could do it on a event hook in this case.\u003C/p>\u003Cp>So when a different event happens inside the platform, we trigger this flow. Let's do the action non blocking, and then we'll do anytime we create a new item in AI migrations, we'll trigger this flow. So I'm just gonna stop here. We'll go to AI migrations and we'll say create a postgrace data model for an LMS. Right?\u003C/p>\u003Cp>So I save this and if I go back to my flow and if I actually make this a little larger where you could see it, I can see my logs over here on the right hand side. Here's our payload. There's the prompt, and then we could see other things like our accountability. Is this a logged in user or not? Great.\u003C/p>\u003Cp>Alright. So let's move on to the next step. Right? Once we've got that prompt, we'd wanna pass that to OpenAI. So let's call, the the the what?\u003C/p>\u003Cp>OpenAI? I I don't even know what to call this. Call AI. We're gonna call the API, and we are looking for the webhook and request URL. So we'll go in and I'm pretty sure this will be a post request, but let's just open up OpenAI.\u003C/p>\u003Cp>Let's open up their platform. Got our API keys. So we're gonna need a new API key. This will be deleted by the time you guys watch, so please don't try to steal my API keys here. But if we look at, their documentation, right, we've got the different models like GPT 4.\u003C/p>\u003Cp>That's probably the the best one that we wanna use. GPT 4 turbo improved instruction following. So maybe that's the model we wanna use. Let's do this. Chat completions.\u003C/p>\u003Cp>So if we're doing this, we curl. Alright. Here's the endpoint that we're probably going to hit. Great. Okay.\u003C/p>\u003Cp>Alright. So if I just separate these 2, we'll drag that over. Get our text generation. So let's copy this URL. There's our chat completions.\u003C/p>\u003Cp>For our headers, we're gonna have content dash type. Got application JSON. Alright. And then we have our authorization. We have bear bear, and I got my API key.\u003C/p>\u003Cp>I'll just copy paste that there. Alright. So in the body of the request is where the meat and potatoes are here. Alright. So for our model, let's use the most expensive one.\u003C/p>\u003Cp>Right? Where's our models at? GPT, GPT 4 turbo. That one has a vision. Okay.\u003C/p>\u003Cp>Great. Let's just use this guy here, 1106. Fancy, fancy. Alright. So we've got our model that we're gonna use, and then we give it a prompt.\u003C/p>\u003Cp>Right? You are a helpful assistant, user assistant. Let me just edit this in my text editor real quick. Alright. So we're gonna do something like this where this is gonna be our trigger dot payload dot prompt.\u003C/p>\u003Cp>We'll delete the rest of these, and then let's give it some system instructions. Right? But let's use chat GPT for that as well. Right? So write some instructions for, chat, GPT 4 model, write some system instructions that will always make it generate SQL queries that, create well well rounded data models for apps as described by users.\u003C/p>\u003Cp>Let's see what it comes back with this. Really weird kind of having a conversation with AI in this case. Blah blah blah. Normalization. Include comments.\u003C/p>\u003Cp>Is this actually going to give me something or not? Let's take a look at where we're at. We got 25 minutes left. Chat GPT here is, like, killing me with the details here. Alright.\u003C/p>\u003Cp>So I guess we could just go with this. Let's just type something out. You are a Postgres and Directus expert full stack developer. Users will describe an application, and you will write a SQL query that will build the data model for that application. Use your expert knowledge to fill, to create a more robust application than the user has described has described.\u003C/p>\u003Cp>Do not use composite keys. Use only UID for primary keys. Call the primary key fields. What? ID.\u003C/p>\u003Cp>Use your best judgment to create the data model. Okay. Sounds good. There's our prompt. Okay.\u003C/p>\u003Cp>We'll just paste this into the body. Hit save. And let's just look at this real quick. There's our payload. Trigger payload prompt.\u003C/p>\u003Cp>Okay. Alright. Let's see what we're gonna get back from here, and then maybe I just want to go in and actually update that. So we'll say update migration. And maybe we can make this a 2 step process where, would maybe we'll just run it.\u003C/p>\u003Cp>We'll see. So we got update data. Let's go into our migration, and then we have trigger dot key. And the payload here is going to be query, and what did we call that? Call let's just do this.\u003C/p>\u003Cp>Just leave that blank for now. Let's give it a shot. Right? Let's see what happens. Please help me build a LMS.\u003C/p>\u003Cp>There are many courses. Each course has several modules and each module has many lessons. Okay. So if I save this, that should call the OpenAI API and return some data or return something. Right?\u003C/p>\u003Cp>Why are we not getting any logs here? Did this actually happen? What's going on here? Alright. Let's try it one more time.\u003C/p>\u003Cp>Or maybe it's still still building. What's going on? Not sure. We are at 21 minutes remaining. Okay.\u003C/p>\u003Cp>Yeah. It just takes a little while. Alright. So here's our payload from OpenAI. Okay.\u003C/p>\u003Cp>So we can already see there's a a problem here. Right? It is returning it's returning additional stuff, which kinda sucks. So we need to adjust our prompt for that. Right.\u003C/p>\u003Cp>You will only return the SQL query. And what was that I saw about JSON mode? Maybe that is something we can use here. Text generation, JSON mode. Okay.\u003C/p>\u003Cp>Yeah. That's probably what we need to enable. How do we do that? Response format type JSON objects. Okay.\u003C/p>\u003Cp>Let's give this a shot, shall we? Alright. So save this. And maybe I will, let me just delete these other prompts that we've got. And, actually, we could probably we've got enough data now that we should be able to pick this up, get choices back.\u003C/p>\u003Cp>Okay. So we would just have to access this, but let's run it one more time just to see. Let me build an LMS. There are many courses. Each course has several modules.\u003C/p>\u003Cp>Blah blah blah blah blah blah. So we are waiting for chat GPT to come back. But while we do that, oh, less than a minute. That's quick. Bad request.\u003C/p>\u003Cp>The dumb dumb. Okay. Response format, JSON, JSON object, messages. Goofed something up. We'll just leave that there.\u003C/p>\u003Cp>This is valid JSON. Looks like it. Let's try again. Alright. Delete.\u003C/p>\u003Cp>Okay. Prompt. Flows. Logs. Okay.\u003C/p>\u003Cp>So now it's doing its thing. What could we do next? Right? We're gonna run that SQL that it returns against that endpoint automatically. Kind of a scary thing, but let's see what we've got.\u003C/p>\u003Cp>Has it returned yet? Nope. Still hasn't returned. Alright. We'll wait on that.\u003C/p>\u003Cp>There we go. Okay. Alright. So was the payload coming back? Okay.\u003C/p>\u003Cp>So there's the SQL statement. Okay. And Alright. So it's just returning the query. When using JSON mode, produce some JSON via message for conversation.\u003C/p>\u003Cp>Response format. Yeah. I feel like we need to set this response format. I'm not sure what I did wrong the last time. Model response format type JSON object.\u003C/p>\u003Cp>This is invalid JSON or something? Not sure. It looks fine to me. Let's try it one more time. Otherwise, we'll just have to do something more interesting.\u003C/p>\u003Cp>I would just have to like replace those last 2 or 3 characters. Clipboard. Help me build an LMS. Cool. Alright.\u003C/p>\u003Cp>If this bricks, we'll just go back to what it was. Yep. Bad request. We cannot parse the JSON body of your request. Yeah.\u003C/p>\u003Cp>I don't I don't know why why it's doing that. Alright. Okay. So, anyway, there's our our data. Alright.\u003C/p>\u003Cp>So what are we getting back from the API when this actually works? We're getting something like this. So let's do a I'm just gonna copy and paste this. So here's our response. K.\u003C/p>\u003Cp>Great. Alright. So let's do an intermediate step where we're just gonna clean this data up. We'll go into run script, clean up query, and we're gonna do what? We're gonna get the data.\u003C/p>\u003Cp>So the query equals data dotco underscore call AI dot what? Dot data dot choices, first item in the array dot message dot content. Okay. And then we are going to, what, remove the first three characters. Right?\u003C/p>\u003Cp>Okay. And let's just use AI again. Right? Fun. Fun.\u003C/p>\u003Cp>Fun. Where did OpenAI go? Alright. Chat GPT. Let's create a new conversation.\u003C/p>\u003Cp>Maybe we could use 3.5 for this because it's faster. Write a JavaScript function to remove the 3 backticksandthe. SQL. Okay. Cool.\u003C/p>\u003Cp>So remove the back ticks in the SQL code. Cool. Alright. There we go. There's our cleaned SQL code function, and then we're just gonna return that.\u003C/p>\u003Cp>Return cleaned SQL code. Alright. So let's make sure that's gonna get what we need. We got data, We got replace content, choices message content. Okay.\u003C/p>\u003Cp>Let's give it a shot. Okay. And now for the next trick, we are going to post that, to our endpoint. Right? So we will grab our local host.\u003C/p>\u003Cp>We're gonna do raw SQL slash run. It's gonna be a post. We're gonna do authorization bearer and this is gonna be our token that we used here. Alright. So this is the token for our direct as user.\u003C/p>\u003Cp>What else do we need? Do we need anything else? Authorization. We probably ought to add content type. Application slash JSON.\u003C/p>\u003Cp>Great. Alright. Okay. So what are we looking at? What this should do is anytime we create a migration it will call OpenAI, ask it to generate that SQL, clean up that SQL, and then run that SQL command against our database.\u003C/p>\u003Cp>So if this goes right, it could basically be an application that could build itself. If it goes wrong, it's gonna blow this thing up entirely. Alright. So we got about 11 minutes left. Let's just test this out.\u003C/p>\u003Cp>Right? We've got our LMS prompt. Let's do it and see. Help me build an LMS. Each course has several modules.\u003C/p>\u003Cp>Each module has several lessons. Alright. Flows. Okay. So it hasn't come back yet.\u003C/p>\u003Cp>It is running. If I pop open Directus, let's open our Docker container. Go to the dashboard. We got Docker running here. We got our 100 apps.\u003C/p>\u003Cp>I can see the activity here, but we're not sure what's going on. Alright. So still running still running. We take a look at our data model. Nothing's happening yet.\u003C/p>\u003Cp>Here we go. Okay. So we got the data back from OpenAI. SQL code is not defined. Okay.\u003C/p>\u003Cp>So some issues, and it's still not returning what we need. The raw SQL query do not return anything else except for the raw SQL query. So sometimes AI is unreliable, and then we have what I must have goofed when I SQL code dot replace. Oh, duh. That's a query.\u003C/p>\u003Cp>Okay. Alright. Let's try again. Just delete this guy. Now this could get really fun where, like, you have AI itself, like on a cron job or something, just manually creating these prompts or automatically creating these prompts.\u003C/p>\u003Cp>So k. That kicks everything off. What are we gonna do in the meantime while we wait? Right? I don't know.\u003C/p>\u003Cp>I've got a a list of dad jokes that we could go through here if you guys want. I struggle with Roman numerals until I get to 159, then it just clicks. C l I x. Yeah. Okay.\u003C/p>\u003Cp>So if I look, here is my raw SQL that ran. If I look at the data model, no. Close. Alright. So let's look at our logs.\u003C/p>\u003Cp>Here's a minute ago. Internal server error cannot read properties of replace. Okay. So there's our raw SQL. Did I forget to actually call the SQL?\u003C/p>\u003Cp>What an idiot. Sometimes you have brain farts. Alright. So I forgot to actually paste in the the query there, so it wasn't running anything. So this is gonna be cleanup query.\u003C/p>\u003Cp>Alright. So if you remember, we are passing a query string into this. So we'll do this, clean up query, and that should solve the problem. Alright. Last time.\u003C/p>\u003Cp>Right? Hopefully this goes through. Help me build the LMS. Fun with AI and code. How we doing on time?\u003C/p>\u003Cp>Got 7 minutes left to prove this concept. All the engineers on our team are probably very upset if they're watching this. They put a lot of hard work into security and and making sure people don't do silly things like this. Alright. So webhook request, bad request, invalid payload.\u003C/p>\u003Cp>Why is it an invalid payload? JSON. Body query. Should we just wrap that in a what do we need to do here? Alright.\u003C/p>\u003Cp>So we reformat dot replace. Should we that bit? I don't know if we need to JSON stringify that. And I guess we could. Return JSON dot stringify.\u003C/p>\u003Cp>Not sure what that's gonna do. And then maybe even let's at least save that query so we can pull it up later. Right? So we'll update that. We'll do this with the, what, cleanup request.\u003C/p>\u003Cp>Cleanup query. Alright. Working on a time crunch here. What's going on? Fields to resolve.\u003C/p>\u003Cp>Save. Edit. Some kind of weird behavior happening. We save JSON Unexpected token. I don't know.\u003C/p>\u003Cp>Let's try it again. Help me build an LMS. Flows. Flows. Flows.\u003C/p>\u003Cp>Alright. So we'll just wait a minute. So also think about these AI. They take a little time, especially the the really fancy models. Right?\u003C/p>\u003Cp>Logs less than a minute ago. Bad request. Why can't we get this to run? Unexpected token. Payload.\u003C/p>\u003Cp>Did it update? Let's just look at the okay. So if we were to pop this in there, assuming we remove this, if we were to pop this inside table plus. Right? So if we open up our database, we go in, we run this SQL query.\u003C/p>\u003Cp>Does this actually work? Run all. Okay. So it doesn't really jive. Okay.\u003C/p>\u003Cp>So whatever we had previously should be what we go back to. Alright. When in doubt, turn to AI? I don't know, man. This one is challenging.\u003C/p>\u003Cp>I I thought we could totally get this done. We are not able to pass this. Let's just do this where we go into our query. We have return. Query.\u003C/p>\u003Cp>That's gonna be our cleaned SQL code. Alright. So we're returning that, and then we're gonna pass that cleanup query directly. So if I go into edit raw value, clean up query. Okay.\u003C/p>\u003Cp>And then I'm just gonna disconnect this piece for now. Man. Okay. Cutting it against the clock. I don't I don't know why I'm always up against the clock on this particular show.\u003C/p>\u003Cp>Alright. Let's try something else. Right? Help me build a CRM. No.\u003C/p>\u003Cp>Build an ecommerce platform for selling watches. Products, categories, prices. Let's just see what it comes back with. Flows less than a minute. Call OpenAI.\u003C/p>\u003Cp>Bad request. Okay. Why is that? Let's just try the standby here. Help me build.\u003C/p>\u003Cp>Alright. Okay. So that's doing its thing. We will try this as well, and otherwise, if it doesn't come back in the next 50 seconds, it feels like a public fail to me. Man, I was really hoping we could get this one to work properly.\u003C/p>\u003Cp>Sometimes it doesn't work out though. This one would be one to do a follow-up on though. Right? Oh, boy. Okay.\u003C/p>\u003Cp>Okay. Okay. Let's see what we got here. Right? Did this actually do what it wanted to do?\u003C/p>\u003Cp>Oh, there we go. How about that? Right? So now we've got our courses. Let's take a look.\u003C/p>\u003Cp>We've got a title and description. If we look at our lessons, what do we have? We've got title and content. We've got our modules that has our course relationship all ready to go for us. Right?\u003C/p>\u003Cp>Woah. Look at that, guys. At the buzzer, we came through. This is now a mighty morphing app Ranger that will build itself. Right?\u003C/p>\u003Cp>What can we do next with this? We could go in and create these prompts, but I could go in and do this, like, call a cron job. Let's just explore it further. I just wanted to take this, just to to one one more stop. So if we do create new prompts and at what, let's just make it whatever.\u003C/p>\u003Cp>I forget the syntax. What is the Google what's the Kron syntax? I wanna say it's like a we use the 6 digits syntax. Okay. Seconds, minutes, hours.\u003C/p>\u003Cp>Okay. So, again, chat gpt. Just for fun, what is create a cron syntax statement that will run every 2 minutes. Okay. Alright.\u003C/p>\u003Cp>So running every 2 minutes. There we go. Got that. So I'm just gonna go back into my Directus application. Where are you?\u003C/p>\u003Cp>Where did it go? Got too much going on. Is it over here? Okay. Alright.\u003C/p>\u003Cp>So we've got our cron syntax. That's gonna trigger and then but what if we call OpenAI again? Okay. I tell you what, let's do this on a follow-up. We're gonna eat up a lot of time here, but, just to prove that this is working again, let's do please help me build an ecommerce platform for selling watches.\u003C/p>\u003Cp>Pricing, let's just leave it at that. Right? Let's see what it does. Call create new prompts 3 minutes ago. Okay.\u003C/p>\u003Cp>This is coming back. And, again, I could watch my directus instance just to see if it is running that SQL statement. I don't see it running just yet. OpenAI is still thinking. But, wow, guys.\u003C/p>\u003Cp>This one was really fun. Pretty crazy what you can generate with AI. I hope you guys enjoyed this one. Stay tuned for more episodes in the series. Did it actually work?\u003C/p>\u003Cp>500 internal server error. What happened? Please make sure. Okay. It tried to kick something else out to to that.\u003C/p>\u003Cp>So it requires some fine tuning on the AI, but I think this is totally viable and totally an interesting project to build an app that can update and change itself. So that's it for this episode. Thanks for sticking around. Catch you on the next episode.\u003C/p>","Hi guys. Welcome back to another episode of 100 apps 100 hours. I'm your host Brian Gillespie, developer advocate at Directus, and today we are tackling the elephant in the room, AI. So we're gonna be building an AI app generator. By that, I mean an app that can update itself with new data models and new interesting ways to work. So what's the inspiration for this? So I've been, over the last couple weeks, I've been seeing all these videos on X or Twitter, whatever it is, that show, this application TL draw where you can go in and sketch something out inside this quick little application, click a button, and it will use the open AI vision API and some of their other tools, I guess, to actually generate real code that makes whatever you sketch come to life. So really excited by that. We're gonna try to replicate something similar here. I'm calling this the mighty morphing App Ranger because I grew up with Power Rangers. It was one of the I think it was one of my first Halloweens. So if you're new to the series, let's dive in. There are 2 rules. We have 60 minutes to plan and build this application, no more no less, and the second rule is just use whatever you have at your disposal. So in this AI episode it should be pretty interesting to see how far we can get with this. One of the ways that I wanna start here is just by charting things out, and we'll try to upload that chart to chat gpt or open AI and and spit something back out that we can use. So without further ado, let's dive in and start building. So like I said, I like to plan everything first. So let's just draw some stuff on our artboard here and kind of sketch out what we want this actual application to do. So the very beginning we're gonna draw a diagram of diagram diagram of an app. Upload, we want to, what, send that to the application. Let's shrink this down. That's really large. Alright, you can tell even though I use Figma quite a bit it's still not my strong suit. So we'll draw a diagram of an app. We will send, use OpenAI Vision to create a data model based on the diagram, and then we're going to update our app based on that code or data model automatically. So we want the app to automatically update itself, but before we dive into that, let's just try to proof the concept out. Right? So let's draw an app. Let's start with something simple like a CRM. Here's our CRM app. What are the different data models or the different tables, the the different collections as we call them inside Directus, which is what we're gonna be using for our back end and what we're gonna build our application with. So we've got contacts. Well, we probably got some organizations. We've got a relationship between those. We probably have some deals, and we can use some arrows for the relationships. And if we just look at contacts and orgs, we probably have maybe like a junction table or something here. I don't think it left enough room, but let's call this, what? Organization contacts. Give that a little more room. Okay. Alright. So there's our basic diagram. Maybe we go in and inspect some fields out for this as well. We got a first name, last name, email, phone, title. K. We've got a name for the organization. We've probably got an address. We've got, what, what else is on the address? I got lost on my Figma file here. So address, name, we got some contacts. We probably got organizations that those contacts belong to. That's probably good enough. For the deal, we probably have a deal name, deal value. And what else? Close date maybe? Alright. So that is a pretty simple CRM app. Let's make our diagram pretty. And now let's just take a screenshot of this and let's just proof this concept out. We can use just the regular chat GPT for this because it it has that underlying vision API that we're gonna use once we start building our app. But I can upload this into chat GPT and say something like this. You are a SQL, post grace, and direct us master. Please create a SQL query, SQL query that will create the data model for the this CRM application as designed in the in the what, in the diagram attached. Please fill in and make the data model more robust. So, honestly, I'm I'm just curious to see if this will actually work to begin with. What happens? Oh, no. Did I lose all of that? Did that really happen? Open this up. Oh, no. Okay. Well, that's a hazards of the job. Right? Restore recently closed. Please, you are a post grace and directus master. No hidden camera tricks on this show. Just fun here. Sometimes you fat finger one of these. Please create a SQL statement SQL query that will create the data model for our app as in the diagram. No. The conversation is not helpful so far. Let's do a new chat. Restore recently closed. Alright. Sometimes the technology doesn't work out so well. Please create a SQL query for Postgres, that's the database we're using, that will create an app create the data model for an app based on the diagram. Alright. I'm gonna copy this just so I don't have to what in the world is going on? Alright. Let's just try this again. Holy moly. Wow. Okay. So we've eaten up a lot of time there just messing with chat gpt. That's a lot of fun, but let's see what this comes up with. Looks like it has to create a SQL data model, SQL code. So if we just look at this, we've got a contacts table. That's good. We've got the organizations table. We've got a associative table. That's a junction table. We have a table for our deals, and then we have some kind of statement that is altering the table. Okay. Please rewrite to use UUID to add a primary key of ID for all the tables and use UIDs. Just want to make sure we all have a primary key of for each one. Alright. Cool. Okay. So now we've got the IDs. We're using the UUID instead of just an integer there. Alright. Great. Okay. So looking at this, we can test it out. Right? How are we gonna test this out? So, what I've got set up, for this app is basically a Docker Compose file that I'm using to generate my back end, which is Directus. And if I pull up my Directus instance and just log in, so we go to admin atexample.com. We do password. We'll log in and if I zoom in just a little bit you can see this is a completely blank app. Nothing here. So what Directus does behind the scenes, it sits alongside your Postgres database and it will introspect that database, which basically means anything that any changes that you make inside that database or any SQL database, doesn't have to just be Postgres, it will mirror those changes in real time. Sounds great. How does it work in application? Alright. So we go in, let's just pull up this database inside TablePlus. Right? So I could see all of my directus tables here. There's no other tables. We've got some items in here for, like, post GIS, just so we can do geo data inside here. But let's just copy and paste this inside the application, or inside table plus, and we're gonna run this SQL query. The only thing that I'm gonna do here is just change this application. I'm gonna change the organization contacts, And what I'm gonna do here, I'm just gonna change this so that there is a ID and that's the primary key instead of a composite key. But other than that, let's just run this thing. So hit run all. Okay. Altered table has been altered. If I refresh, we can see that we have created those different tables and, you know, the associated columns. Great. Now if I load up Directus, I can see that Directus, just refreshing the screen, has recognized that those tables are inside our database. So I can go through and click to configure these tables. We can see that my different fields are coming through, and if we click on those we can configure what the actual UI would look like for these things. So if I just go through and click on a few of them, maybe we want to hide this specific field. Directus really easily allows you to update this data and the presentation of it. So basically now we're just controlling the form that displays when we add a new contact. So let's just edit that a little bit, put the title down there. Great. Yeah. So if we go in, we refresh, we've got our contacts, and now I could go in and add my contact. Great. 555-555555. Looking great. Brian@directus dotio, and I'm a developer advocate. Cool. So now if I go into table plus here, we can also see there's my record. Right? So Directus is really nice in that it mirrors everything, but it also gives me a REST based API. So I could go in and any of those collections, if I were to, just give read access to just the general public, we could also create different user roles here. But if I were to go in and just copy my address I could do something like this where do items slash contacts and boom I get it ready to use REST or GraphQL API that we can use to build our back end with. That's great, this is really cool. How can we take this further, right, how can we make this application able to adjust itself, right, what does that actually look like? So Directus has built in automation, using flows. So flows you could set up, any type of simple or complex automation for your data. You can make third party API calls, receive incoming webhooks, all of those great things. And I'm thinking we can use a combination of flows and chat GPT or the OpenAI API, that's a mouthful, to actually be able to send this a prompt, have it update the data model automatically. Now there are multiple ways to adjust the schema or your data model of a Directus application. So if we look at the documentation, Directus actually has some schema endpoints where you can go and snapshot your data model. So you make a request, your schema Let's see if we see the sample response. This is the diff, so it actually takes a snapshot of the existing schema and then compares that, with what you've got and comes up with a difference. But it's basically a bunch of JSON data that represents the data model inside our database, inside our direct instance. It is a fairly specific and complex syntax, though. So I'm not sure about OpenAI being able to actually parse this. You know, I'm really concerned about the accuracy for this because it's kind of a a niche syntax that it has to adhere to. Right? So maybe we could go the SQL route because it seems like, OpenAI understands SQL pretty well. Just just guessing. I'm not sure if it does or not. I guess we'll find out. Right? So how can we actually do this? Direct is it doesn't allow you to run raw SQL, rightfully so. Easy way to blow up your database. But I think that's how we're gonna have to do this. So what can we do? Let's reach for a custom extension. So I'm just gonna go to the docs. We're going to create an extension, and so let's pull up our application. I'll just open this up. Alright, so we've got the npx command. Yes, We will create a Directus extension. And there are a couple different types of Directus extensions that you can create. I say a couple, I mean there's a lot of Directus Extensions, but what we're going to do is create an endpoint that we can call and run that actual with a SQL query provided by OpenAI and run that actual query. So first off foremost, Nat, maybe we can get, like, a big disclaimer on here. Do not do this sort of thing in production. Our engineering and security team will probably both be mad at me for this, but we're going to do the custom endpoint extension. What are we gonna call this? Let's just call it raw SQL is the name of our extension. We can use typescript, that's fine. And boom. So Directus will go through and scaffold out this extension. I could see it here inside my raw SQL. Cool. Alright. So now what I wanna do, I'm just gonna drag and drop this into our extensions endpoints folder directory and I'm gonna do one other thing here as well. When we actually build this extension, I'm just gonna drop it into the root of that raw SQL directory, instead of a distribution or dist directory, because it has to be inside the root directory for Directus to pick up on that. Alright. So now we're gonna go to extensions slash what endpoints/rawsequel. Okay. We're gonna do npm run dev, or it says npm run dev. So let's run that. And one of the other things that I've got, if you are working locally with Directus, you're building extensions, probably one of the things that you wanna do inside your config is set up this extensions auto reload, which basically means anytime that you rebuild that extension, which, you know, we're doing here, it will reload that without having to restart the direct assistance for you, which is nice when you are building. Alright. So if we take a look at our existing extension here, we hit save. It's gonna build that endpoint for us. And looks like Directus is reloading extensions. So now if I go to our Directus URL, local host 8055, and I do raw SQL, we get this hello world. Right? Great. But what do we want to actually do now? We want to actually run a a raw SQL query. So how do we actually make that work? How do we get that done? Underneath the hood, Directus is currently using a query builder called connects or nex or that. I never know how to say this thing, if you pronounce the k or not. But it does have a raw it has a raw query object or a raw method that you can call to actually run a raw statement against your database. Again, giant disclaimer, the lawyers say don't do this in production because it is super easy to blow up your database, and also don't trust AI fully to build your application for you. Lots of red flags on this one, but should be interesting nonetheless. Right? So what are we gonna do? We are going to look for our custom endpoints, and Directus gives us access to that underlying database instance. So the endpoint receives the router and the context. So if we do something like this, we've got our context, if I can actually type, and we're gonna do let's make this a post route and where we let's just call it raw SQL run. Alright. Cool. So we'll wrap this in a, like, a try catch. And what are we gonna do? We are going to pick up the query from the body. Yeah. Okay. And we are going to do what? We're going to do something like this where we have context dot databasecontext.database.raw, and then we're gonna run that query, and then we are going to return the result. Alright. Let's do our catch error, error dot message. Now let's see what happens if I refresh. Raw SQL, nothing happens on this, but let's go to run. Raw SQL does not exist. That's that's great. We want it to not work when we run that, but let's just open up something like Postman, so we could quickly test this. I I'm not really a curl guy. I always struggle with the command line. Alright. So we will pop this in. We've got raw SQL run, and we should be passing what? A raw JSON. K. And we've got our query. And let's just say select from what contacts. Alright. We hit posts. Raw SQL run does not exist. Why is that? Rawsequel/run. Why do we not exist? Great question. We are 36 minutes in. Raw SQL run does not exist. Alright. Let's see. We're getting a wait can't oh, the, this needs to be what? Async. Okay. Boom. Alright. So we just ran a raw database query. So you could see that query here, and it is returning the result from that query. Alright. Great. One of the other things that we could do here just to make this a little more robust would be to use the accountability object that gets included inside our services here. Where is this at? So each request we could take a look at the accountability. So we do something like this where only an admin user could do this. So if rec_.accountability ability dotuser.isisadmin? Is that what it is? I think that's it. Only admins can run raw SQL queries. Did I spell that right? Req.accountability. Let's see. Only admins can run raw SQL queries, so now we have to be logged in to actually run that. And, cool. So now if I go to our admin user and just generate an access token, and if I pass that in our authentication here as a bearer token. Okay. Cool. So we've added a little bit of auth to this endpoint to, you know, run our our SQL queries against the production database. Great. Alright. So now we've got our application here, and I refresh. I could see we've got contacts, deals, organizations. How we doing on time? Let's go in and let's add a new collection. We're just gonna call it, what, app, transformations, mighty morphin' power rangers. You know, we could probably even call it, like, AI migrations or something like that. Right? We'll generate a type for it. Do we really need these extra fields? Maybe we can do that. Alright. Alright. So let's give a let's have a here's the prompt. And I'm trying to think of ways that we could make this more robust. Let's see the SQL. Let's call this a query, and let's add another field. Maybe we could ask OpenAI to give us a way to undo. Okay. Alright. So now if we do this, Yeah. Maybe we should have had that status field as well. Has it been applied or not? So let's just do a drop down. So status, we'll do applied, unapplied, unapplied. Okay. And then any default values here will be unapplied. Cool. Alright. Great. Okay. So now we've got our AI migrations. You know, I I could go with the chart thing, but we could also make this fun where anybody can create new data for those or new applications. So let's do let's start by building our flow first. Alright. So flow flows are how we automate data inside Directus. So let's create a new flow, and we'll say call open AI, and we could trigger this a couple of different ways. We could trigger it manually or we could do it on a event hook in this case. So when a different event happens inside the platform, we trigger this flow. Let's do the action non blocking, and then we'll do anytime we create a new item in AI migrations, we'll trigger this flow. So I'm just gonna stop here. We'll go to AI migrations and we'll say create a postgrace data model for an LMS. Right? So I save this and if I go back to my flow and if I actually make this a little larger where you could see it, I can see my logs over here on the right hand side. Here's our payload. There's the prompt, and then we could see other things like our accountability. Is this a logged in user or not? Great. Alright. So let's move on to the next step. Right? Once we've got that prompt, we'd wanna pass that to OpenAI. So let's call, the the the what? OpenAI? I I don't even know what to call this. Call AI. We're gonna call the API, and we are looking for the webhook and request URL. So we'll go in and I'm pretty sure this will be a post request, but let's just open up OpenAI. Let's open up their platform. Got our API keys. So we're gonna need a new API key. This will be deleted by the time you guys watch, so please don't try to steal my API keys here. But if we look at, their documentation, right, we've got the different models like GPT 4. That's probably the the best one that we wanna use. GPT 4 turbo improved instruction following. So maybe that's the model we wanna use. Let's do this. Chat completions. So if we're doing this, we curl. Alright. Here's the endpoint that we're probably going to hit. Great. Okay. Alright. So if I just separate these 2, we'll drag that over. Get our text generation. So let's copy this URL. There's our chat completions. For our headers, we're gonna have content dash type. Got application JSON. Alright. And then we have our authorization. We have bear bear, and I got my API key. I'll just copy paste that there. Alright. So in the body of the request is where the meat and potatoes are here. Alright. So for our model, let's use the most expensive one. Right? Where's our models at? GPT, GPT 4 turbo. That one has a vision. Okay. Great. Let's just use this guy here, 1106. Fancy, fancy. Alright. So we've got our model that we're gonna use, and then we give it a prompt. Right? You are a helpful assistant, user assistant. Let me just edit this in my text editor real quick. Alright. So we're gonna do something like this where this is gonna be our trigger dot payload dot prompt. We'll delete the rest of these, and then let's give it some system instructions. Right? But let's use chat GPT for that as well. Right? So write some instructions for, chat, GPT 4 model, write some system instructions that will always make it generate SQL queries that, create well well rounded data models for apps as described by users. Let's see what it comes back with this. Really weird kind of having a conversation with AI in this case. Blah blah blah. Normalization. Include comments. Is this actually going to give me something or not? Let's take a look at where we're at. We got 25 minutes left. Chat GPT here is, like, killing me with the details here. Alright. So I guess we could just go with this. Let's just type something out. You are a Postgres and Directus expert full stack developer. Users will describe an application, and you will write a SQL query that will build the data model for that application. Use your expert knowledge to fill, to create a more robust application than the user has described has described. Do not use composite keys. Use only UID for primary keys. Call the primary key fields. What? ID. Use your best judgment to create the data model. Okay. Sounds good. There's our prompt. Okay. We'll just paste this into the body. Hit save. And let's just look at this real quick. There's our payload. Trigger payload prompt. Okay. Alright. Let's see what we're gonna get back from here, and then maybe I just want to go in and actually update that. So we'll say update migration. And maybe we can make this a 2 step process where, would maybe we'll just run it. We'll see. So we got update data. Let's go into our migration, and then we have trigger dot key. And the payload here is going to be query, and what did we call that? Call let's just do this. Just leave that blank for now. Let's give it a shot. Right? Let's see what happens. Please help me build a LMS. There are many courses. Each course has several modules and each module has many lessons. Okay. So if I save this, that should call the OpenAI API and return some data or return something. Right? Why are we not getting any logs here? Did this actually happen? What's going on here? Alright. Let's try it one more time. Or maybe it's still still building. What's going on? Not sure. We are at 21 minutes remaining. Okay. Yeah. It just takes a little while. Alright. So here's our payload from OpenAI. Okay. So we can already see there's a a problem here. Right? It is returning it's returning additional stuff, which kinda sucks. So we need to adjust our prompt for that. Right. You will only return the SQL query. And what was that I saw about JSON mode? Maybe that is something we can use here. Text generation, JSON mode. Okay. Yeah. That's probably what we need to enable. How do we do that? Response format type JSON objects. Okay. Let's give this a shot, shall we? Alright. So save this. And maybe I will, let me just delete these other prompts that we've got. And, actually, we could probably we've got enough data now that we should be able to pick this up, get choices back. Okay. So we would just have to access this, but let's run it one more time just to see. Let me build an LMS. There are many courses. Each course has several modules. Blah blah blah blah blah blah. So we are waiting for chat GPT to come back. But while we do that, oh, less than a minute. That's quick. Bad request. The dumb dumb. Okay. Response format, JSON, JSON object, messages. Goofed something up. We'll just leave that there. This is valid JSON. Looks like it. Let's try again. Alright. Delete. Okay. Prompt. Flows. Logs. Okay. So now it's doing its thing. What could we do next? Right? We're gonna run that SQL that it returns against that endpoint automatically. Kind of a scary thing, but let's see what we've got. Has it returned yet? Nope. Still hasn't returned. Alright. We'll wait on that. There we go. Okay. Alright. So was the payload coming back? Okay. So there's the SQL statement. Okay. And Alright. So it's just returning the query. When using JSON mode, produce some JSON via message for conversation. Response format. Yeah. I feel like we need to set this response format. I'm not sure what I did wrong the last time. Model response format type JSON object. This is invalid JSON or something? Not sure. It looks fine to me. Let's try it one more time. Otherwise, we'll just have to do something more interesting. I would just have to like replace those last 2 or 3 characters. Clipboard. Help me build an LMS. Cool. Alright. If this bricks, we'll just go back to what it was. Yep. Bad request. We cannot parse the JSON body of your request. Yeah. I don't I don't know why why it's doing that. Alright. Okay. So, anyway, there's our our data. Alright. So what are we getting back from the API when this actually works? We're getting something like this. So let's do a I'm just gonna copy and paste this. So here's our response. K. Great. Alright. So let's do an intermediate step where we're just gonna clean this data up. We'll go into run script, clean up query, and we're gonna do what? We're gonna get the data. So the query equals data dotco underscore call AI dot what? Dot data dot choices, first item in the array dot message dot content. Okay. And then we are going to, what, remove the first three characters. Right? Okay. And let's just use AI again. Right? Fun. Fun. Fun. Where did OpenAI go? Alright. Chat GPT. Let's create a new conversation. Maybe we could use 3.5 for this because it's faster. Write a JavaScript function to remove the 3 backticksandthe. SQL. Okay. Cool. So remove the back ticks in the SQL code. Cool. Alright. There we go. There's our cleaned SQL code function, and then we're just gonna return that. Return cleaned SQL code. Alright. So let's make sure that's gonna get what we need. We got data, We got replace content, choices message content. Okay. Let's give it a shot. Okay. And now for the next trick, we are going to post that, to our endpoint. Right? So we will grab our local host. We're gonna do raw SQL slash run. It's gonna be a post. We're gonna do authorization bearer and this is gonna be our token that we used here. Alright. So this is the token for our direct as user. What else do we need? Do we need anything else? Authorization. We probably ought to add content type. Application slash JSON. Great. Alright. Okay. So what are we looking at? What this should do is anytime we create a migration it will call OpenAI, ask it to generate that SQL, clean up that SQL, and then run that SQL command against our database. So if this goes right, it could basically be an application that could build itself. If it goes wrong, it's gonna blow this thing up entirely. Alright. So we got about 11 minutes left. Let's just test this out. Right? We've got our LMS prompt. Let's do it and see. Help me build an LMS. Each course has several modules. Each module has several lessons. Alright. Flows. Okay. So it hasn't come back yet. It is running. If I pop open Directus, let's open our Docker container. Go to the dashboard. We got Docker running here. We got our 100 apps. I can see the activity here, but we're not sure what's going on. Alright. So still running still running. We take a look at our data model. Nothing's happening yet. Here we go. Okay. So we got the data back from OpenAI. SQL code is not defined. Okay. So some issues, and it's still not returning what we need. The raw SQL query do not return anything else except for the raw SQL query. So sometimes AI is unreliable, and then we have what I must have goofed when I SQL code dot replace. Oh, duh. That's a query. Okay. Alright. Let's try again. Just delete this guy. Now this could get really fun where, like, you have AI itself, like on a cron job or something, just manually creating these prompts or automatically creating these prompts. So k. That kicks everything off. What are we gonna do in the meantime while we wait? Right? I don't know. I've got a a list of dad jokes that we could go through here if you guys want. I struggle with Roman numerals until I get to 159, then it just clicks. C l I x. Yeah. Okay. So if I look, here is my raw SQL that ran. If I look at the data model, no. Close. Alright. So let's look at our logs. Here's a minute ago. Internal server error cannot read properties of replace. Okay. So there's our raw SQL. Did I forget to actually call the SQL? What an idiot. Sometimes you have brain farts. Alright. So I forgot to actually paste in the the query there, so it wasn't running anything. So this is gonna be cleanup query. Alright. So if you remember, we are passing a query string into this. So we'll do this, clean up query, and that should solve the problem. Alright. Last time. Right? Hopefully this goes through. Help me build the LMS. Fun with AI and code. How we doing on time? Got 7 minutes left to prove this concept. All the engineers on our team are probably very upset if they're watching this. They put a lot of hard work into security and and making sure people don't do silly things like this. Alright. So webhook request, bad request, invalid payload. Why is it an invalid payload? JSON. Body query. Should we just wrap that in a what do we need to do here? Alright. So we reformat dot replace. Should we that bit? I don't know if we need to JSON stringify that. And I guess we could. Return JSON dot stringify. Not sure what that's gonna do. And then maybe even let's at least save that query so we can pull it up later. Right? So we'll update that. We'll do this with the, what, cleanup request. Cleanup query. Alright. Working on a time crunch here. What's going on? Fields to resolve. Save. Edit. Some kind of weird behavior happening. We save JSON Unexpected token. I don't know. Let's try it again. Help me build an LMS. Flows. Flows. Flows. Alright. So we'll just wait a minute. So also think about these AI. They take a little time, especially the the really fancy models. Right? Logs less than a minute ago. Bad request. Why can't we get this to run? Unexpected token. Payload. Did it update? Let's just look at the okay. So if we were to pop this in there, assuming we remove this, if we were to pop this inside table plus. Right? So if we open up our database, we go in, we run this SQL query. Does this actually work? Run all. Okay. So it doesn't really jive. Okay. So whatever we had previously should be what we go back to. Alright. When in doubt, turn to AI? I don't know, man. This one is challenging. I I thought we could totally get this done. We are not able to pass this. Let's just do this where we go into our query. We have return. Query. That's gonna be our cleaned SQL code. Alright. So we're returning that, and then we're gonna pass that cleanup query directly. So if I go into edit raw value, clean up query. Okay. And then I'm just gonna disconnect this piece for now. Man. Okay. Cutting it against the clock. I don't I don't know why I'm always up against the clock on this particular show. Alright. Let's try something else. Right? Help me build a CRM. No. Build an ecommerce platform for selling watches. Products, categories, prices. Let's just see what it comes back with. Flows less than a minute. Call OpenAI. Bad request. Okay. Why is that? Let's just try the standby here. Help me build. Alright. Okay. So that's doing its thing. We will try this as well, and otherwise, if it doesn't come back in the next 50 seconds, it feels like a public fail to me. Man, I was really hoping we could get this one to work properly. Sometimes it doesn't work out though. This one would be one to do a follow-up on though. Right? Oh, boy. Okay. Okay. Okay. Let's see what we got here. Right? Did this actually do what it wanted to do? Oh, there we go. How about that? Right? So now we've got our courses. Let's take a look. We've got a title and description. If we look at our lessons, what do we have? We've got title and content. We've got our modules that has our course relationship all ready to go for us. Right? Woah. Look at that, guys. At the buzzer, we came through. This is now a mighty morphing app Ranger that will build itself. Right? What can we do next with this? We could go in and create these prompts, but I could go in and do this, like, call a cron job. Let's just explore it further. I just wanted to take this, just to to one one more stop. So if we do create new prompts and at what, let's just make it whatever. I forget the syntax. What is the Google what's the Kron syntax? I wanna say it's like a we use the 6 digits syntax. Okay. Seconds, minutes, hours. Okay. So, again, chat gpt. Just for fun, what is create a cron syntax statement that will run every 2 minutes. Okay. Alright. So running every 2 minutes. There we go. Got that. So I'm just gonna go back into my Directus application. Where are you? Where did it go? Got too much going on. Is it over here? Okay. Alright. So we've got our cron syntax. That's gonna trigger and then but what if we call OpenAI again? Okay. I tell you what, let's do this on a follow-up. We're gonna eat up a lot of time here, but, just to prove that this is working again, let's do please help me build an ecommerce platform for selling watches. Pricing, let's just leave it at that. Right? Let's see what it does. Call create new prompts 3 minutes ago. Okay. This is coming back. And, again, I could watch my directus instance just to see if it is running that SQL statement. I don't see it running just yet. OpenAI is still thinking. But, wow, guys. This one was really fun. Pretty crazy what you can generate with AI. I hope you guys enjoyed this one. Stay tuned for more episodes in the series. Did it actually work? 500 internal server error. What happened? Please make sure. Okay. It tried to kick something else out to to that. So it requires some fine tuning on the AI, but I think this is totally viable and totally an interesting project to build an app that can update and change itself. So that's it for this episode. Thanks for sticking around. Catch you on the next episode.","published",[139],{"people_id":140},{"id":141,"first_name":142,"last_name":143,"avatar":144,"bio":145,"links":146},"791e1503-1d88-463d-9347-0b9192933576","Bryant","Gillespie","9013afc8-e8d7-4182-9b18-44db08117bb9","Developer Advocate at Directus",[147,149],{"url":131,"service":148},"website",{"service":150,"url":151},"github","https://github.com/bryantgillespie",[],{"id":154,"number":155,"year":156,"episodes":157,"show":168},"56dda5ff-2c3a-41ce-ae3a-580d6101026b",1,"2023",[158,159,160,161,162,122,163,164,165,166,167],"cb4e067f-9507-4e18-ab9a-435565f9e653","8434838a-8e4f-489a-8da2-fbf10de5de6a","c997b25e-400c-4350-bba4-f63853d844f7","1109be0d-8ab5-479b-a052-8ad30d9ffb1c","dcb952e0-7d13-43ea-9b1c-f2ca63efd07d","8fed0eee-43f6-4767-8b77-79da7a059821","a311b57d-34e7-4073-9cf3-6a8c6c0f8b85","9271a4fc-addf-4400-9591-f2f2ec07bd79","30c48566-52cd-4728-a633-ca9675acc959","c067c012-5fe1-4d70-8946-8bfc8e1b22c0",{"title":169,"tile":170},"100 Apps In 100 Hours","fb0f9d45-be21-4634-94d4-2ef1cc5146f2",{"id":172,"slug":173,"season":174,"vimeo_id":175,"description":176,"tile":177,"length":178,"resources":8,"people":8,"episode_number":155,"published":179,"title":180,"video_transcript_html":181,"video_transcript_text":182,"content":8,"seo":183,"status":137,"episode_people":184,"recommendations":186},"9a3a8ffa-a27b-421c-93cf-3da2dcb726e9","crm","14fda5f2-95de-4dbe-a4e2-3fd956c21c19","936325383","In this intense one-hour challenge, watch as Bryant incredibly builds a full-featured custom CRM from the ground up using Directus. He builds contacts, organizations, deal pipelines, activities, and more.","f6b880a0-5cd2-45ad-beff-3117f0a78581",56,"2024-04-19","Mission: Customer Relationship Manager","\u003Cp>Speaker 0: Hi. Welcome back to another episode of 100 apps. 100 hours where we build some of your favorite apps or try to rebuild some of your favorite apps in 1 hour or less, or get publicly humiliated trying. Alright. Super excited to be back for another season.\u003C/p>\u003Cp>If you are new to 100 apps 100 hours, there are 2 basic rules. Number 1, you have 60 minutes to build and plan and build, no more, no less. So when that clock strikes 0, that's it. And then the second rule is there are no other rules. Use whatever you have at your disposal to complete the functionality.\u003C/p>\u003Cp>That's it. Let's dive into this episode. So today, I've got a custom CRM. Tools like Salesforce, Pipedrive, HubSpot, they really need no introduction. Everybody needs a CRM.\u003C/p>\u003Cp>A lot of them, depending on your purposes, may be way too much for what you need or they may not be specific enough for your industry. So we are going to build our own custom CRM in 1 hour or less. Let's do it baby. Alright. So, let's start the clock and away we go.\u003C/p>\u003Cp>So, when we plan our CRM, what do we need out of a CRM? Right? What kind of functionality do we want to see inside our CRM? So basically, we want to manage all of our contacts. We want to manage all the different organizations those contacts belong to.\u003C/p>\u003Cp>What else? We wanna manage our sales pipeline. Manage sales pipeline. That's gonna be deals or activities. We want to be able to track activities and follow-up.\u003C/p>\u003Cp>So this seems like a a pretty good set of functionality for a basic CRM. I'm sure we might embellish this a little bit depending on how far we get, but let's dive into actually fleshing out what the data model just might look like for something like this. So we'll drag a nice square up here. We're gonna have contacts. We could just call those people.\u003C/p>\u003Cp>I'm a big fan of that. We're gonna have organizations. There's definitely gonna be a relationship between those 2, but I I feel like this could be a many to many relationship because one contact could belong to multiple organizations, and and some CRMs make that a little more difficult to do than others. What else do we have? We're gonna have deals or opportunities.\u003C/p>\u003Cp>Deals is kind of a a standard nomenclature. We're gonna have activities that are attached to what, those are attached to organizations, they're attached to contacts, they're probably also gonna be attached to deals. What else are we going to have? We're probably going to have some sales reps, those are going to be our users inside our accounts. This looks pretty good for a a base set of functionality.\u003C/p>\u003Cp>Let's dive in and actually start building something. So, I'm just gonna pull up my Directus instance, that's what we're using on the back end. Totally blank instance, we'll zoom in on this, and let's start building. Right? Let's create our first collection.\u003C/p>\u003Cp>Let's start with contacts. So we're gonna give this a contacts as the name. For the primary key, let's use generated UUID. What kind of fields do we wanna add for our contacts? Right?\u003C/p>\u003Cp>When we think of contacts, we're gonna need name, email, but we may also wanna track when this was updated, when it was created. Directus makes that super easy. And one tip that you may not have realized if you've used Directus before, is I can go in and change these to be whatever I want. So if I wanted this to be created at and created by, I could go in and update those, change them to be whatever I want. Updated at, updated by.\u003C/p>\u003Cp>That's the the nomenclature that you're used to. Do we actually need a sort for our contacts? I don't think. Let's just go ahead and save this. Alright.\u003C/p>\u003Cp>So for our contacts, we're gonna add a first name. Great. We'll make that a string. Let's do half width for that. I can go in and click the three little dots here and just quickly duplicate this field to save myself some time.\u003C/p>\u003Cp>And, of course, as I am plotting away on this, Directus underneath is mirroring all these changes to my database schema. So we've got our first name, we've got our last name, we're gonna need a email or an email. Great. Let's require this value. And, you know, I could even go as far as, like, making this unique if I I wanted to have some type of unique identifier to match these up with.\u003C/p>\u003Cp>Alright. As far as an interface, you know, I could get fancy with this and we could add a little email icon like an at symbol. That's great. I think everything else is good. You know, one of the other nice things that's built into Directus is I can go in and add my own custom reg x validation.\u003C/p>\u003Cp>So, you know, I can put in some type of reg x that matches email addresses. I don't have one of those handy here, but we'll go ahead and keep marching along here. So we got first name, we got last name, we got email. We'll probably have a phone number at some point, but let's let's go for, like, a job title, probably. I'm trying to think of all the standard fields.\u003C/p>\u003Cp>Maybe we have some notes. We could set that to be a text area field. I don't really need formatting for those. Great. Job title.\u003C/p>\u003Cp>And maybe honestly, let let's strip this out because I'm I'm thinking about that many to many relationship with organizations, and we may have, different titles at different organizations. So I'll show you how we can handle that coming up. Alright. So we got first name, we got email. We could go ahead and add phone number as a string.\u003C/p>\u003Cp>Phone or phone number. Let's just keep it short. We'll say phone. Let's give it a phone icon. Great.\u003C/p>\u003Cp>K. Phone, email. We'll put email above phone. Cool. Looks nice.\u003C/p>\u003Cp>Alright. Let's just take a look. Right? We've got first name. Go ahead.\u003C/p>\u003Cp>Oh, I was gonna add my wife here, but she's gonna get mad at me if I misspell it. Right? Ashley atexample.com. I had a phone number, 555-5555. Here's some nice notes for our contact.\u003C/p>\u003Cp>These are looking great. Right? We've got our contact in here. I could potentially sort and filter this a a hundred different ways, maybe change the layout. But let's dive into the next collection that we wanna set up, our organizations.\u003C/p>\u003Cp>So we'll have organizations, again, for the primary key, I'm gonna use ID and just use generated UUID as the type. And, again, I could change created at I could change this structure for these system fields to be whatever I want. Created by, date updated, that's gonna be updated at, if I can actually spell. And we use this one as well and call it updated by. Great.\u003C/p>\u003Cp>Okay. So now we've got an organization. For our organization, what are we gonna have? We're gonna have a name of the organization. Right?\u003C/p>\u003Cp>Probably, some addresses for that organization. Right? We could have multiple. So that's where we might reach into an another bag of just a different table. What else are we gonna have for an organization?\u003C/p>\u003Cp>You know, let's just do something like country in case we want to, even, like, automatically route new organizations to a specific sales rep. Potentially, we've got a name. What are we gonna have? We're gonna have a website. You know, we may have a a logo if we wanna track that for a company.\u003C/p>\u003Cp>So let's do an image file. Let's call it logo. You know, you might even add things like brand color, things like that if if you're really in tracking that. And then we'll just add another section for notes. This is gonna be a text area field with the type text, and we'll hit save.\u003C/p>\u003Cp>So we've got our organizations. We've got our contacts. Let's make a link between the 2. Right? So how do we go about that?\u003C/p>\u003Cp>Depends on how you want the data to actually be structured. Right? In an application, depending on the setup, maybe a user can only belong to one specific organization. But often inside of CRM and maybe I am working with the local little league. I am a member of the board there, and then I'm also a developer advocate here at Directus or, you know, a a founder at Better Side Shop.\u003C/p>\u003Cp>So a lot of different relationships that is gonna be modeled with the mini to mini relationship inside Directus. Here's how we set that up. It's gonna be pretty easy. We'll just go to create field. We'll look for a many to many relationship.\u003C/p>\u003Cp>And because we're on organizations, our key here is gonna be contact for the or contacts, I should say. Our related collection is gonna be contacts and then we'll just go through and paint by numbers here. Do we want to show these in a list or a table? I'm good either way. We definitely want to show a link to that item so we can get to that specific contact, but I'm gonna pop open this advanced field creation mode just to show you that you do have control over the naming of the junction collection and the individual fields within that.\u003C/p>\u003Cp>So, the default setup here is just going to take this table name and this table name or this collection name and this collection name and marry the 2 together and we end up with organizations underscore contacts. Works for me. We'll leave that autofill set up. And then on the reverse, I'm gonna add that many to many field to the contacts as well. We're gonna keep that as the organizations.\u003C/p>\u003Cp>Alright? In this case, we may have a sort field. So we wanna control the the sorting for contacts, like who's 1st, 2nd, 3rd, that could be helpful in, like, a primary contact situation. And then we have some of our relational triggers. Right?\u003C/p>\u003Cp>On deselect of organizations' contacts, what do we want to do here? Yeah. Maybe I want to delete that association. So I'll set these to cascade. Great.\u003C/p>\u003Cp>So now that back out, we can see we've got our contacts field here. If we go into, yep, looks like we've got, an extra contact table created. I might have typed something wrong. But, so we can see contacts there in the organizations. And then on our contact, I should see organizations here.\u003C/p>\u003Cp>Let's just add an organization. Right? So let's say my wife worked at Tesla. That's in the United States. And, you know, we could add the website, logo, notes.\u003C/p>\u003Cp>I could choose existing contacts. But I'm just going to go ahead and say 1. Right? So now, I could see I've got that organization here, but it's showing an ID, which is not super helpful. Right?\u003C/p>\u003Cp>So I can go into the organization itself and control the display template where I have a name. I could even potentially add the logo to that if I wanted. So now if we check it again, we can see, okay, here's the organizations that they're a part of. Right? Now, you also have the ability to manage the data inside that junction collection.\u003C/p>\u003Cp>Right? So if I go in and and like I said, I may have a different job title at all these different organizations. So I could go in and add a new field here inside this junction collection for job title. Great. Alright.\u003C/p>\u003Cp>So, now if we go into Tesla, we can see the job title is down here at the bottom, but Directus also gives me an easy way to control where that displays. So if I go to our many to many relationship inside our contacts, I can go to maybe we wanna add a sort field for this just to make sure. And then if I go to the interface, I can control where my junction fields are located. So I can put this at the top, which should be great. Okay.\u003C/p>\u003Cp>So now if we just check that out one more time. Right. Now I could see director of DirectUs. So now I could give a specific job title to this specific person within that organization. I could go in and add a new organization as well, like, hey, the little league.\u003C/p>\u003Cp>We can say a board member. Right? Little League Baseball is the name of the organization. Great. Okay.\u003C/p>\u003Cp>So now there my wife is a part of 2 organizations, different job titles for each. Right? Alright. How are we looking on time? We're looking great.\u003C/p>\u003Cp>Let's move on to our next collection that we want to set up. That's gonna be what? Our deals, our activities, what do we want to set up next? Let's go for deals or opportunities. Deals is probably the standard naming for this.\u003C/p>\u003Cp>So that's what we'll stick with. Could be opportunities. Could be something else. That's fine. We'll do created at.\u003C/p>\u003Cp>Just to keep the same structure, we'll do created by. And by adding these, whenever this record is created, it's going to populate a timestamp and a user. The same thing for Updated, whenever it gets updated or who updates it, it's gonna populate that info for us. Updated by. Great.\u003C/p>\u003Cp>Okay. So our deals what we're gonna have for our deal? We'll probably have a name or a title of the deal. Deal name sounds great. It could be good to prefix some of these sometimes if you're dealing with a lot of nested relational data.\u003C/p>\u003Cp>So you get used to seeing name in there a 100 times. Could be confusing whether that's the actual deal name or the organization name or the contact name. So we'll just save the deal name. What else are we gonna have on this deal? Right?\u003C/p>\u003Cp>We'll probably have a average dollar amount or a potential dollar amount. Let's set that to be, decimal. Deal value. We can add a nice little dollar sign to the icon to make this look nice and pretty. And then we are going to have some related fields.\u003C/p>\u003Cp>But before we do that, let's just add, like, some deal notes or something like that. Sounds great. Okay. So we got a deal name, we got a deal value, you know, maybe these are side by side, not a big deal. Let's go in, and now we want to add a relationship to the organization and probably to our primary contact for this deal.\u003C/p>\u003Cp>So those are both gonna be many to one relationships. And we'll set a key of organization here. So the related collection, we'll set that to be organizations. Great. And I could control the display template here.\u003C/p>\u003Cp>We'll just use the name and we hit save. So now we've got an organization that we're gonna tie to the deal. I'll make that half width. And then we also want to add a primary contact. So we'll just say primary contact.\u003C/p>\u003Cp>The related collection here is going to be the Contacts collection. And for the Display template, let's use first name, last name, and let's use this format, which I think is typically how it displays inside an email client where you have the email address inside these less than or greater than SQL or symbols. So let's save that. We've got our primary contact. We've got our notes.\u003C/p>\u003Cp>We've got our deal. What do we need next? Right? We need to track where that deal is at. So we could use, like, a a status field as, like, a drop down for that potentially.\u003C/p>\u003Cp>Right? What's the status of this particular deal? Blah blah blah. Or we could do something else where we have a relationship to a deal stage or our individual pipeline. Right?\u003C/p>\u003Cp>Because then we may want to, potentially have separate pipelines or we we wanna give more control back to our users, so the the sales reps or the sales manager to control that actual pipeline without getting into the admin section. So how can we do that? Right? Looks like I've got a little extra couple of fields that I've been creating here inadvertently. Alright, so now let's add a new collection.\u003C/p>\u003Cp>Let's just call it Deal Stages. We want to manage what are the stages of a particular deal. As far as the optional fields, maybe I don't really not super concerned with these, deal stages. We're gonna call this the name of this particular stage. Maybe we wanna give it a color so we can add some color to it.\u003C/p>\u003Cp>And, you know, I guess we could even give it an icon if we wanted to. We could play around with that and see what that looks like. Deal stages. Let's make both of those half width. And, now, let's just go through and map some of these particular stages.\u003C/p>\u003Cp>Right? And I and I, you know, I've got this s on here. One of the other things that you could do inside Directus, you can't control the translations for all of these. So even if you're working in English, on the the back end, you may want to use prefix tables to keep things nice and organized as a developer. This is a great way to control what displays via the interface to your actual users within the application.\u003C/p>\u003Cp>So I could just call this deal stages or pipeline stages, whatever makes a lot of sense here, and hit save. Alright. So it still shows me deals_stages here, but when I start looking inside the actual app, there we go, we can see our pipeline stages. So, let's create our first one. Right?\u003C/p>\u003Cp>Let's just say, New, this is inbound. Do we have a symbol? We do have a symbol for this. Right? So let's say red.\u003C/p>\u003Cp>This is we need to take action on this. What's next? Right? Qualified, maybe? Contacted?\u003C/p>\u003Cp>Or, let's say assigned. Right? We forgot to assign those to a sales rep. Assign assignment. That's a nice looking icon for that.\u003C/p>\u003Cp>Then we'll say qualified. You know, if we're doing software sales, like, a check mark. Okay. What else? Demo.\u003C/p>\u003Cp>Let's save green. Oh, let's make this orange. Do we have like a demo? Maybe a TV? What have we got?\u003C/p>\u003Cp>Yeah, there we go. That works for a demo as far as the icons. And then we are going to set, like, a last stage, like, proposal. Great. Let's just go gray.\u003C/p>\u003Cp>Cool. Document. Awesome. Alright. So I can control the way that these are displayed through my actual settings, here.\u003C/p>\u003Cp>So I can go into each specific one. And on the display tab, let's display a colored dot for the color. And then for the icon, we're just gonna display the actual icon. Alright. Great.\u003C/p>\u003Cp>If we take a look now, I can see what these actually look like. Cool. And then for my display template for the deal stages, I may even go in and just mirror that same structure where I have a color, then I have an icon with a name. Okay. So, now, what we've done, we've effectively given control over these, and we probably actually need a sort on these as well, so so we can control that value.\u003C/p>\u003Cp>So, let's go in and quickly add a sort. That's going to be an integer value. And because this is coming after the fact, after we've created this actual table, I'm just gonna scroll down a little bit, and I will have a sort field here. So we'll choose sort just to make sure we manage that. And now I should be able to drag and drop these, in whatever order that I like.\u003C/p>\u003Cp>Again, we're giving control back to the users of our custom CRM. We're definitely gonna prevent them from getting inside the admin so they don't mess with the data model. But here, we give them the ability to control what those pipeline stages are gonna be. So now we need to add those to our actual deals. Right?\u003C/p>\u003Cp>So we're gonna go into our deals. We're going to create a many to one relationship. And, you know, this is going to be the deal stage or just stage. And we're gonna use deals underscore stages as the related collection. We can open this up and and just see what's going on.\u003C/p>\u003Cp>I I don't really need to create that inverse relationship. I could. Doesn't make a ton of sense, though. I'm not sure. Alright.\u003C/p>\u003Cp>Display related values, validation. Let's keep it very simple, and then we're gonna use our deal stage. Alright. So now if I go into our deals Why is this oh, it goes in the machine. I don't know what's going on on this particular example, but it keeps creating additional fields that we don't need.\u003C/p>\u003Cp>Alright. So let's create a new deal. Alright. This is gonna be a new deal. We can see, hey, there's our stage.\u003C/p>\u003Cp>That looks great. The deal name is 100 apps, 100 hours contract. It's worth $1,000,000. So we'll add that. How many zeros do we have there?\u003C/p>\u003Cp>1, 2, 3, 4, 5, 6. Alright. Let's make this for the Little League team. Great. My wife is the primary contact.\u003C/p>\u003Cp>Here's some notes on this deal. Great. Did we save it? Numeric value and deals is out of range. Uh-oh.\u003C/p>\u003Cp>Wouldn't be a 100 apps, a 100 hours without, some type of issue. Right? So let's take a look at this. The deal value, what did I set? Precision and scale.\u003C/p>\u003Cp>Maybe we bumped this up way up. Probably could have gone with integers for this as well. Do I have a minimum and a maximum? I I don't have that set. So not entirely sure what's going on.\u003C/p>\u003Cp>Let's just try this again. 100 apps, 100 hours contract, 1 100,000, 1,000,000. Try it again. Save. Okay.\u003C/p>\u003Cp>Alright. So now, we've got this particular deal set up. This is kind of an underwhelming view. Right? And, again, I might control how these things display.\u003C/p>\u003Cp>So if I just go into our deals, we go to the deal stage. If I wanna control how it displays, I can go in and set this. So let's do the color, do the icon, do the name, just because, I'm a I'm a very visual person, and I suspect, a lot of your end users may be as well. We still don't see the organization there. So, again, we'll go back.\u003C/p>\u003Cp>We'll adjust our organization. We wanna display related values. We want to show the name of that specific organization. And let's get even fancier and maybe we wanna show that logo. I'm gonna click on this and then you'll see this one that has a little magic beside it that's a thumbnail.\u003C/p>\u003Cp>That's just a shortcut for, like, a nice thumbnail sized image instead of loading the actual image size. So I donit have a logo for this company, but I could quickly find 1. Alright. So, we'll just copy this image address, go back to our instance, let's load up that specific company, they who shall not be named, Hit import. Oh, that's a data URL.\u003C/p>\u003Cp>Let's find an actual URL that we can just copy. Great. Let's try this again. We can import via URL. Cool.\u003C/p>\u003Cp>And now when I'm looking at my deals, I can see the logo of that organization. That makes it really handy, when I'm working on a deal just to have that extra extra visual reinforcement. Now, when I'm looking at deals, I may want to set up like a traditional pipeline type of view. So I could do that really easily just by switching the layout here inside Directus. So we'll change this to a Kanban layout.\u003C/p>\u003Cp>Let's group by the deal stage and the group title is gonna be the name. So I can see new assigned qualified demo. We have that Kanban view that we're used to. As far as the text, what do we wanna look at? We want to control, let's say, when this was created.\u003C/p>\u003Cp>I'm not really concerned about that. The tags, what do we want to set that to be? Do we want a card image? Honestly, this looks okay. Do we need an actual user on here?\u003C/p>\u003Cp>Probably not. Cool. Alright. We'll just keep it as is and let's let's start fleshing this out a little more as well. Right?\u003C/p>\u003Cp>So within a deal, when I'm working this deal, we're going to have activities assigned to this as it moves across the pipeline. And, for that, let's just go in and add a new table, a new collection, we're going to call it activities. Activities, I think that's the correct spelling. We use generated UID, we'll do created at, createdby, create up, updatedat, updated by. Great.\u003C/p>\u003Cp>And, in this case, maybe we do give a status for this particular activity. Right. Status, great. Let's give a name of the activity, probably a type of activity. Makes sense.\u003C/p>\u003Cp>We use a drop down for that. It seems pretty straightforward or, you know, maybe they're not gonna need to change the activity types on a a very frequent basis. So we'll call it the activity type, and let's add a couple choices to this. So this is a relatively new addition to Directus within the drop down, the ability to have an icon and a color. So let's say text is a phone call, value is a phone.\u003C/p>\u003Cp>One of the other cool things if you want to translate this value, you could use this phone t, or this dollar sign t for a translatable string. And then anytime you have users who are using the app within a different language, inside the settings, you can control all those custom translations. We'll take a look at that in a moment. Let's call this what? Phone call.\u003C/p>\u003Cp>The value could just be phone call, depending on how I wanna store this. Right? It may have an underscore within it as well. It could just be the same thing. Either way.\u003C/p>\u003Cp>Look for that phone. Great. And I could even add a color for that if needed. Maybe I just wanna keep these all the same. Phone call, we'll call it a meeting.\u003C/p>\u003Cp>And for this, do we have a meeting icon? Right? We calendar looks nice. Maybe we want to track demos separate from meetings. Alright.\u003C/p>\u003Cp>So we drag a demo. That was the TV icon that we had. Great. What else are we gonna need? Like, an email?\u003C/p>\u003Cp>Not sure I would Yeah. Maybe, like, a follow-up email. We wanna reschedule that. And then we'll do an email. Great.\u003C/p>\u003Cp>Demo, phone call. I think I'm gonna set this to be underscore value. Cool. Alright. Looking good.\u003C/p>\u003Cp>Activity type. Okay. What else are we going to need for this? We need a due date for this activity and the status, we can use as as is. It's published draft, archived.\u003C/p>\u003Cp>What do I really care about this activity? It probably isn't completed or not. Right? So maybe we scrap status, and maybe we just go for a toggle instead. Right?\u003C/p>\u003Cp>Hey. Is this completed? The default value, great. For our label, maybe we change it to completed and give it a color of color on. Color off is red, just to show that it has not been completed.\u003C/p>\u003Cp>Great. And let's just clean up this form a little bit, making sure everything looks nice. Okay. We are going to add a date for this, and let's set a specific date and time that this thing occurred. Call it due date, end date.\u003C/p>\u003Cp>Due date seems reasonable, but when do we actually need to complete this specific task? Okay. So now we've got an activity. We want to, link this to our actual deal so we can track those. Right?\u003C/p>\u003Cp>So, what I'm gonna do in this case is create a mini to one relationship because this activity could only belong to a single deal. Right? We're gonna use the key of deal. The related collection, we'll set that to deals. We'll show the name of that deal.\u003C/p>\u003Cp>Maybe we show the actual organization as well so I can actually dig into the related collections and and show values from that? Excuse me one second. Sorry about that. Cool. Alright.\u003C/p>\u003Cp>So now, what I forgot to do is create that inverse relationship. You can actually set that up via direct us when you're creating that relationship. But now I can also just go into our deals, not deal. We'll go into our deals, and now we're gonna create a one to many relationship back to those activities. So we'll call this key of activities.\u003C/p>\u003Cp>We've got activities. The foreign key will be deal. That already exists. Maybe we wanna show these in a table. We'll choose the columns, due date, Name, Due Date, Activity Type.\u003C/p>\u003Cp>Seems pretty savvy. And I can even filter these, right? So if I wanted to see just activities where they were not completed. We can enable search and filtering and show a link to these. We'll take a look at what all these look like in just a moment.\u003C/p>\u003Cp>But we've forgotten one important step through this whole process is, hey. We need somebody to assign these deals to. Right? So, let's add a many to one relationship. We'll call that the deal owner or, you know, you potentially say who this is assigned to.\u003C/p>\u003Cp>Like, the deal owners, again, kind of standard naming in these scenarios. And for the related collection here, we're gonna use directus underscore users. So these are gonna be actual users of the application that we're assigning this to. Invalid payload collections can't start with direct us users. Oh, deal owner.\u003C/p>\u003Cp>Let's go to our related collection, and let's get direct us underscore users. And, in this case, we're gonna show the first name, last name. We may back up and do an avatar as well. So just the the thumbnail, the avatar, I could move these around just by using edit raw value. You'll see these are just, the standard mustache syntax that you see throughout Directus as well.\u003C/p>\u003Cp>Excuse me. Let me get this a drink of water. I'm actually dying. That's a turn of fate. Okay.\u003C/p>\u003Cp>Alright. So we're gonna save this. Deal owner already exists in deals. Okay. Alright.\u003C/p>\u003Cp>Great. Let's move this around. Maybe we make deal stage half width. We slide deal owner up there. Let's actually take a look at this now.\u003C/p>\u003Cp>I should be able to assign folks. So let's create a new user. We'll just call it sales rep. Salesrep@example.com. And maybe we give them a nice avatar.\u003C/p>\u003Cp>Right? Sales rep, avatar. Let's just see what Google comes back with. John's inside sales rep. Yeah.\u003C/p>\u003Cp>This looks this is perfect. This is my guy right here. Alright. There's his avatar. We'll just save that.\u003C/p>\u003Cp>And now we can see who we've assigned this particular deal to. And now maybe within the deal card user here, I wanna show who that deal owner is. Great. Mister sales rep. Looking good.\u003C/p>\u003Cp>Alright. One of the other things that we need to do on our activities, we probably got a an owner of that activity or assigned to. It's been assigned to somebody on the team. Again, that's going to be assigned to a direct us user. We'll save that.\u003C/p>\u003Cp>And lots going on here. Just some type of weird glitch. I could see a couple of extra collections. I'll just remove these. Alright.\u003C/p>\u003Cp>Cool. So now weive got a deal. Weive got a table full of activities. I can go ahead and add these, like, follow-up on proposal. This is assigned to Mr.\u003C/p>\u003Cp>Sales Rep. We've got the activity type. We can see that's going to be, just a quick phone call. This is completed. We can see that conditional, conditional formatting for that.\u003C/p>\u003Cp>And we can add a due date of, let's say, the next Friday. Great. Save that. Keep editing. Cool.\u003C/p>\u003Cp>Alright. So, now, we've got the basic inner workings of a CRM. Right? We've got our deals, we've got contacts, we've got organizations, we've got our different pipeline stages. Right?\u003C/p>\u003Cp>If I wanted to organize these things a bit, we could go in and add different icons for each of these. So, you know, maybe we set some people icons for our contacts. We've got our organizations. Do we have an organization option? Let's look and see.\u003C/p>\u003Cp>Business. Is there a business? There you go. That looks somewhat like a business. We've got activities.\u003C/p>\u003Cp>Maybe this will be like a checklist. Cool. We've got our deals. Let's make those the money. Dollar signs, that's great.\u003C/p>\u003Cp>And then, deal stages, to me this is like a settings. Right? So I could create a new folder, let's just call it settings folder. You don't necessarily have to add this, but maybe we just do to keep it clean. And we look for, like, a settings icon just to use here.\u003C/p>\u003Cp>That's great. This one looks magical. Settings suggest. Right? And, again, I can change the name of this to just say settings.\u003C/p>\u003Cp>So, it still creates this collection, but, we can call it whatever we want. So we'll drag this up within settings folder. We'll drag deal stages and, this will be, what, like the Kanban view. There we go. Awesome.\u003C/p>\u003Cp>Okay. So now we get a little more organization to this. One of the other things that you might do and, you know, that you use all the time within ACRM are saved views. Right? So Directus gives you that ability with bookmarks.\u003C/p>\u003Cp>We'll just go in to the top here and maybe I want to sort by a specific sales rep. Right? Like, the deal owner is, a specific person and specific name is sales rep. That's the only one at this point. But I could go in and create a bookmark for this, and we could call it, deals sales rep Man.\u003C/p>\u003Cp>And we can change this up, give it a color. Now, within that collection, now we can see we've got our deals for Sales Rep Van, and I could save that bookmark. So even if I go into the main deals view, and maybe we change this back to a table view, which could be easier for, you know, maybe a sales manager or something who's controlling this. So I had deal owner back to this as well. Now if I go back, deal sales rep man view, boom, there it is.\u003C/p>\u003Cp>It's saved. I could go in and update this if I wanted to as well. So now we've got our pipeline. When we go into each one of the deal, we have our name, we've got our organization, we've got the notes, we've got our activities. You know, we can mark these activities off as completed.\u003C/p>\u003Cp>That seems like a great CRM baseline. Let's let's take a look at where we're at. Right? We got, like, 16 minutes on the clock. This feels like a win.\u003C/p>\u003Cp>I don't I don't know if we wanna run that one, let's discuss where we could go from here, right, maybe we want to automatically send some emails when it hits a certain stage in the pipeline. So let's just call these things done. Right? We can manage all of our contacts. We can manage all of our organizations.\u003C/p>\u003Cp>We can manage our sales pipelines. We can track our activities and follow-up. So let's say, you know, we get a new deal inbound. Maybe we want to automatically assign that to a a particular person, or we want to send a notification to our sales rep when that assignment happens. Let's figure out how to do that.\u003C/p>\u003Cp>Right? So I'm gonna go in. Let's just create a new deal. We'll say actually, let's wait a moment. Let's go into our flows.\u003C/p>\u003Cp>This is a good example. Right? Whenever a a new deal comes inbound, we want to send an email notification to our sales rep to to let them know. Alright. So we're gonna create a new flow.\u003C/p>\u003Cp>We'll just call it new inbound deal. Pretty straightforward. We could change this to the new symbol if we want to and just do a trigger setup. So what are we going to choose here? Directus gives you a ton of different options, as far as what to use when you're creating a flow.\u003C/p>\u003Cp>In this case, we're going to use the event hook. So when a certain event happens inside the platform, we wanna trigger an automation. The type that we're gonna choose here is action non blocking because we don't, the the filter allows you to basically either adjust the payload when a new deal gets created or a new event happens. Action non blocking, again, that runs after a create or update action. So in this case, let's do the items dot create.\u003C/p>\u003Cp>Whenever we're gonna trigger this based on the deals collection. And cool. Alright, so now I'm just going to save this, right? I'm going to go in and let's create a new deal inside the system. It's in the new stage.\u003C/p>\u003Cp>We're gonna assign this to mister sales rep. New deal automation. And we add, let's set this one to be for the little league. Again, we choose a specific contact, add some notes, and, maybe, the deal value, we'll just ballpark it at $5,000. Great.\u003C/p>\u003Cp>If I go back, now I can see that in my logs, I've got this flow. Here's the payload of this particular flow, and we could see who the deal owner is on this particular deal. How do we send an email notification to that specific owner? Right? I'm just gonna take this, copy it, and I open up just my Versus code editor where I've got my 100 apps, just, Docker Compose file here set up to run this locally.\u003C/p>\u003Cp>I'm just going to save that in case I need it. And, next, let's flesh this out a little bit. Right? So you can see the data we're getting back here. We're probably gonna need to look up that specific user.\u003C/p>\u003Cp>We can find them there. Those are the deal owner. We could send them a notification inside the app or we could send them a notification via email. Right? There's 2 options there.\u003C/p>\u003Cp>We've got notification. In this case, we've got the UUID of the user that we're going to send that to. So, you know, potentially, you want to send that in app. App. In this case, if there's a new deal, they're probably gonna be in their inbox.\u003C/p>\u003Cp>We're going to send that new deal to them. But let's actually find that email address first, though. Alright. So we're gonna read data from a specific collection. Let's call it Find User is the actual step we're gonna do here.\u003C/p>\u003Cp>For the permissions, let's just give full access. And, for the collection, you can see I don't have the Directus Users collection here, but I can go in and edit my raw value and just use Directus underscore users. And for our IDs, right, what are we gonna put here? So if I open this back up, we're gonna use the trigger. Payload.\u003C/p>\u003Cp>Dealowner. So we'll do this, we'll do trigger dot payload dot deal underscore owner. And I'm gonna wrap this in mustache syntax. And let's try this again. So we'll read the user.\u003C/p>\u003Cp>Let's go ahead and maybe just add this send email as well. So we'll send the email, and this is gonna be the read underscore user. We're using the key that we set of the previous operation within that flow. Readuser. Email should be.\u003C/p>\u003Cp>We'll input that. New deal assigned to you. And hey. Read_user.firstname. We've assigned a new deal to you.\u003C/p>\u003Cp>And then I can even go through and add those different variables if I wanted to for things like the deal name or the deal ID and and add a link back to that. So let's just try trigger. Payload.deal_name. Great. We'll save it.\u003C/p>\u003Cp>And now let's just test this out. I'm not actually sure if I've got emails set up here locally on this particular instance though, so that could be fun. But we should be able to actually see if this runs. In light of that bit of news, because it just looking at my configuration here, I do not have email configured here locally. So let's let's detach that one, and let's just test the notifications.\u003C/p>\u003Cp>Send notification. Cool. Send notification. The find_user.id. Cool.\u003C/p>\u003Cp>Full access. And it was a new deal assigned. We'll just add the key here. So that'll be trigger dot payload. Or no.\u003C/p>\u003Cp>Actually, it may be something like this where we have key. Alright. Let's just take a look just to make sure. Alright. Within our payload, we can see the key there.\u003C/p>\u003Cp>That's good. In that case, we probably didn't need to get the actual user there, but that's okay. I'm just gonna copy this message that we set here. Just paste that. And let's take a look at where this gets us.\u003C/p>\u003Cp>Alright. So we've got a new inbound deal. We're going to read the data of the user that we've assigned that to and we're going to send a notification to that specific person. Alright. Now we've got a new deal, deal stage.\u003C/p>\u003Cp>Let's just set it to new. Deal owner. I'm gonna choose myself here so we could test this notification. Say deal name. Test deal.\u003C/p>\u003Cp>It's worth $6,000,000. Very nice deal. We got a primary contact. Here's some notes. We'll just save that and let's see what happens.\u003C/p>\u003Cp>Right? We go to our Flow, we get a new inbound deal, we can see our logs, send notification, recipient is so and so. Hey, undefined. We've sent you a new test deal. Underscore first name.\u003C/p>\u003Cp>Why did that not come back? Read underscore oh, that's why I used the wrong key. Sometimes that happens. Find user dot first name. Great.\u003C/p>\u003Cp>So now, if I look and I check my activity log, I can see that I've got this new deal signed. Here's the notification for that. And if I click on it where it says view content, it should take me to that specific test deal. Great. Awesome.\u003C/p>\u003Cp>Let's call that a win. Right? We've got our custom CRM built out. We've set up some automation for this. We could go further and flesh this out a a ton of different ways.\u003C/p>\u003Cp>Right? If we talk about it, like, we could go through and send emails to our actual primary contact when it reaches a certain stage, that would be fairly easy to do using direct dis flows. You know, we could maybe even go as far as, like, a future future iteration of this could be setting up an inbox to parse incoming emails like a BCC functionality and add these to those specific deals as well. But this this feels really good. I'm I'm calling this a win.\u003C/p>\u003Cp>That's our custom CRM. Thanks for joining me on this episode of 100 apps, 100 hours. We'll catch you on the next one.\u003C/p>","Hi. Welcome back to another episode of 100 apps. 100 hours where we build some of your favorite apps or try to rebuild some of your favorite apps in 1 hour or less, or get publicly humiliated trying. Alright. Super excited to be back for another season. If you are new to 100 apps 100 hours, there are 2 basic rules. Number 1, you have 60 minutes to build and plan and build, no more, no less. So when that clock strikes 0, that's it. And then the second rule is there are no other rules. Use whatever you have at your disposal to complete the functionality. That's it. Let's dive into this episode. So today, I've got a custom CRM. Tools like Salesforce, Pipedrive, HubSpot, they really need no introduction. Everybody needs a CRM. A lot of them, depending on your purposes, may be way too much for what you need or they may not be specific enough for your industry. So we are going to build our own custom CRM in 1 hour or less. Let's do it baby. Alright. So, let's start the clock and away we go. So, when we plan our CRM, what do we need out of a CRM? Right? What kind of functionality do we want to see inside our CRM? So basically, we want to manage all of our contacts. We want to manage all the different organizations those contacts belong to. What else? We wanna manage our sales pipeline. Manage sales pipeline. That's gonna be deals or activities. We want to be able to track activities and follow-up. So this seems like a a pretty good set of functionality for a basic CRM. I'm sure we might embellish this a little bit depending on how far we get, but let's dive into actually fleshing out what the data model just might look like for something like this. So we'll drag a nice square up here. We're gonna have contacts. We could just call those people. I'm a big fan of that. We're gonna have organizations. There's definitely gonna be a relationship between those 2, but I I feel like this could be a many to many relationship because one contact could belong to multiple organizations, and and some CRMs make that a little more difficult to do than others. What else do we have? We're gonna have deals or opportunities. Deals is kind of a a standard nomenclature. We're gonna have activities that are attached to what, those are attached to organizations, they're attached to contacts, they're probably also gonna be attached to deals. What else are we going to have? We're probably going to have some sales reps, those are going to be our users inside our accounts. This looks pretty good for a a base set of functionality. Let's dive in and actually start building something. So, I'm just gonna pull up my Directus instance, that's what we're using on the back end. Totally blank instance, we'll zoom in on this, and let's start building. Right? Let's create our first collection. Let's start with contacts. So we're gonna give this a contacts as the name. For the primary key, let's use generated UUID. What kind of fields do we wanna add for our contacts? Right? When we think of contacts, we're gonna need name, email, but we may also wanna track when this was updated, when it was created. Directus makes that super easy. And one tip that you may not have realized if you've used Directus before, is I can go in and change these to be whatever I want. So if I wanted this to be created at and created by, I could go in and update those, change them to be whatever I want. Updated at, updated by. That's the the nomenclature that you're used to. Do we actually need a sort for our contacts? I don't think. Let's just go ahead and save this. Alright. So for our contacts, we're gonna add a first name. Great. We'll make that a string. Let's do half width for that. I can go in and click the three little dots here and just quickly duplicate this field to save myself some time. And, of course, as I am plotting away on this, Directus underneath is mirroring all these changes to my database schema. So we've got our first name, we've got our last name, we're gonna need a email or an email. Great. Let's require this value. And, you know, I could even go as far as, like, making this unique if I I wanted to have some type of unique identifier to match these up with. Alright. As far as an interface, you know, I could get fancy with this and we could add a little email icon like an at symbol. That's great. I think everything else is good. You know, one of the other nice things that's built into Directus is I can go in and add my own custom reg x validation. So, you know, I can put in some type of reg x that matches email addresses. I don't have one of those handy here, but we'll go ahead and keep marching along here. So we got first name, we got last name, we got email. We'll probably have a phone number at some point, but let's let's go for, like, a job title, probably. I'm trying to think of all the standard fields. Maybe we have some notes. We could set that to be a text area field. I don't really need formatting for those. Great. Job title. And maybe honestly, let let's strip this out because I'm I'm thinking about that many to many relationship with organizations, and we may have, different titles at different organizations. So I'll show you how we can handle that coming up. Alright. So we got first name, we got email. We could go ahead and add phone number as a string. Phone or phone number. Let's just keep it short. We'll say phone. Let's give it a phone icon. Great. K. Phone, email. We'll put email above phone. Cool. Looks nice. Alright. Let's just take a look. Right? We've got first name. Go ahead. Oh, I was gonna add my wife here, but she's gonna get mad at me if I misspell it. Right? Ashley atexample.com. I had a phone number, 555-5555. Here's some nice notes for our contact. These are looking great. Right? We've got our contact in here. I could potentially sort and filter this a a hundred different ways, maybe change the layout. But let's dive into the next collection that we wanna set up, our organizations. So we'll have organizations, again, for the primary key, I'm gonna use ID and just use generated UUID as the type. And, again, I could change created at I could change this structure for these system fields to be whatever I want. Created by, date updated, that's gonna be updated at, if I can actually spell. And we use this one as well and call it updated by. Great. Okay. So now we've got an organization. For our organization, what are we gonna have? We're gonna have a name of the organization. Right? Probably, some addresses for that organization. Right? We could have multiple. So that's where we might reach into an another bag of just a different table. What else are we gonna have for an organization? You know, let's just do something like country in case we want to, even, like, automatically route new organizations to a specific sales rep. Potentially, we've got a name. What are we gonna have? We're gonna have a website. You know, we may have a a logo if we wanna track that for a company. So let's do an image file. Let's call it logo. You know, you might even add things like brand color, things like that if if you're really in tracking that. And then we'll just add another section for notes. This is gonna be a text area field with the type text, and we'll hit save. So we've got our organizations. We've got our contacts. Let's make a link between the 2. Right? So how do we go about that? Depends on how you want the data to actually be structured. Right? In an application, depending on the setup, maybe a user can only belong to one specific organization. But often inside of CRM and maybe I am working with the local little league. I am a member of the board there, and then I'm also a developer advocate here at Directus or, you know, a a founder at Better Side Shop. So a lot of different relationships that is gonna be modeled with the mini to mini relationship inside Directus. Here's how we set that up. It's gonna be pretty easy. We'll just go to create field. We'll look for a many to many relationship. And because we're on organizations, our key here is gonna be contact for the or contacts, I should say. Our related collection is gonna be contacts and then we'll just go through and paint by numbers here. Do we want to show these in a list or a table? I'm good either way. We definitely want to show a link to that item so we can get to that specific contact, but I'm gonna pop open this advanced field creation mode just to show you that you do have control over the naming of the junction collection and the individual fields within that. So, the default setup here is just going to take this table name and this table name or this collection name and this collection name and marry the 2 together and we end up with organizations underscore contacts. Works for me. We'll leave that autofill set up. And then on the reverse, I'm gonna add that many to many field to the contacts as well. We're gonna keep that as the organizations. Alright? In this case, we may have a sort field. So we wanna control the the sorting for contacts, like who's 1st, 2nd, 3rd, that could be helpful in, like, a primary contact situation. And then we have some of our relational triggers. Right? On deselect of organizations' contacts, what do we want to do here? Yeah. Maybe I want to delete that association. So I'll set these to cascade. Great. So now that back out, we can see we've got our contacts field here. If we go into, yep, looks like we've got, an extra contact table created. I might have typed something wrong. But, so we can see contacts there in the organizations. And then on our contact, I should see organizations here. Let's just add an organization. Right? So let's say my wife worked at Tesla. That's in the United States. And, you know, we could add the website, logo, notes. I could choose existing contacts. But I'm just going to go ahead and say 1. Right? So now, I could see I've got that organization here, but it's showing an ID, which is not super helpful. Right? So I can go into the organization itself and control the display template where I have a name. I could even potentially add the logo to that if I wanted. So now if we check it again, we can see, okay, here's the organizations that they're a part of. Right? Now, you also have the ability to manage the data inside that junction collection. Right? So if I go in and and like I said, I may have a different job title at all these different organizations. So I could go in and add a new field here inside this junction collection for job title. Great. Alright. So, now if we go into Tesla, we can see the job title is down here at the bottom, but Directus also gives me an easy way to control where that displays. So if I go to our many to many relationship inside our contacts, I can go to maybe we wanna add a sort field for this just to make sure. And then if I go to the interface, I can control where my junction fields are located. So I can put this at the top, which should be great. Okay. So now if we just check that out one more time. Right. Now I could see director of DirectUs. So now I could give a specific job title to this specific person within that organization. I could go in and add a new organization as well, like, hey, the little league. We can say a board member. Right? Little League Baseball is the name of the organization. Great. Okay. So now there my wife is a part of 2 organizations, different job titles for each. Right? Alright. How are we looking on time? We're looking great. Let's move on to our next collection that we want to set up. That's gonna be what? Our deals, our activities, what do we want to set up next? Let's go for deals or opportunities. Deals is probably the standard naming for this. So that's what we'll stick with. Could be opportunities. Could be something else. That's fine. We'll do created at. Just to keep the same structure, we'll do created by. And by adding these, whenever this record is created, it's going to populate a timestamp and a user. The same thing for Updated, whenever it gets updated or who updates it, it's gonna populate that info for us. Updated by. Great. Okay. So our deals what we're gonna have for our deal? We'll probably have a name or a title of the deal. Deal name sounds great. It could be good to prefix some of these sometimes if you're dealing with a lot of nested relational data. So you get used to seeing name in there a 100 times. Could be confusing whether that's the actual deal name or the organization name or the contact name. So we'll just save the deal name. What else are we gonna have on this deal? Right? We'll probably have a average dollar amount or a potential dollar amount. Let's set that to be, decimal. Deal value. We can add a nice little dollar sign to the icon to make this look nice and pretty. And then we are going to have some related fields. But before we do that, let's just add, like, some deal notes or something like that. Sounds great. Okay. So we got a deal name, we got a deal value, you know, maybe these are side by side, not a big deal. Let's go in, and now we want to add a relationship to the organization and probably to our primary contact for this deal. So those are both gonna be many to one relationships. And we'll set a key of organization here. So the related collection, we'll set that to be organizations. Great. And I could control the display template here. We'll just use the name and we hit save. So now we've got an organization that we're gonna tie to the deal. I'll make that half width. And then we also want to add a primary contact. So we'll just say primary contact. The related collection here is going to be the Contacts collection. And for the Display template, let's use first name, last name, and let's use this format, which I think is typically how it displays inside an email client where you have the email address inside these less than or greater than SQL or symbols. So let's save that. We've got our primary contact. We've got our notes. We've got our deal. What do we need next? Right? We need to track where that deal is at. So we could use, like, a a status field as, like, a drop down for that potentially. Right? What's the status of this particular deal? Blah blah blah. Or we could do something else where we have a relationship to a deal stage or our individual pipeline. Right? Because then we may want to, potentially have separate pipelines or we we wanna give more control back to our users, so the the sales reps or the sales manager to control that actual pipeline without getting into the admin section. So how can we do that? Right? Looks like I've got a little extra couple of fields that I've been creating here inadvertently. Alright, so now let's add a new collection. Let's just call it Deal Stages. We want to manage what are the stages of a particular deal. As far as the optional fields, maybe I don't really not super concerned with these, deal stages. We're gonna call this the name of this particular stage. Maybe we wanna give it a color so we can add some color to it. And, you know, I guess we could even give it an icon if we wanted to. We could play around with that and see what that looks like. Deal stages. Let's make both of those half width. And, now, let's just go through and map some of these particular stages. Right? And I and I, you know, I've got this s on here. One of the other things that you could do inside Directus, you can't control the translations for all of these. So even if you're working in English, on the the back end, you may want to use prefix tables to keep things nice and organized as a developer. This is a great way to control what displays via the interface to your actual users within the application. So I could just call this deal stages or pipeline stages, whatever makes a lot of sense here, and hit save. Alright. So it still shows me deals_stages here, but when I start looking inside the actual app, there we go, we can see our pipeline stages. So, let's create our first one. Right? Let's just say, New, this is inbound. Do we have a symbol? We do have a symbol for this. Right? So let's say red. This is we need to take action on this. What's next? Right? Qualified, maybe? Contacted? Or, let's say assigned. Right? We forgot to assign those to a sales rep. Assign assignment. That's a nice looking icon for that. Then we'll say qualified. You know, if we're doing software sales, like, a check mark. Okay. What else? Demo. Let's save green. Oh, let's make this orange. Do we have like a demo? Maybe a TV? What have we got? Yeah, there we go. That works for a demo as far as the icons. And then we are going to set, like, a last stage, like, proposal. Great. Let's just go gray. Cool. Document. Awesome. Alright. So I can control the way that these are displayed through my actual settings, here. So I can go into each specific one. And on the display tab, let's display a colored dot for the color. And then for the icon, we're just gonna display the actual icon. Alright. Great. If we take a look now, I can see what these actually look like. Cool. And then for my display template for the deal stages, I may even go in and just mirror that same structure where I have a color, then I have an icon with a name. Okay. So, now, what we've done, we've effectively given control over these, and we probably actually need a sort on these as well, so so we can control that value. So, let's go in and quickly add a sort. That's going to be an integer value. And because this is coming after the fact, after we've created this actual table, I'm just gonna scroll down a little bit, and I will have a sort field here. So we'll choose sort just to make sure we manage that. And now I should be able to drag and drop these, in whatever order that I like. Again, we're giving control back to the users of our custom CRM. We're definitely gonna prevent them from getting inside the admin so they don't mess with the data model. But here, we give them the ability to control what those pipeline stages are gonna be. So now we need to add those to our actual deals. Right? So we're gonna go into our deals. We're going to create a many to one relationship. And, you know, this is going to be the deal stage or just stage. And we're gonna use deals underscore stages as the related collection. We can open this up and and just see what's going on. I I don't really need to create that inverse relationship. I could. Doesn't make a ton of sense, though. I'm not sure. Alright. Display related values, validation. Let's keep it very simple, and then we're gonna use our deal stage. Alright. So now if I go into our deals Why is this oh, it goes in the machine. I don't know what's going on on this particular example, but it keeps creating additional fields that we don't need. Alright. So let's create a new deal. Alright. This is gonna be a new deal. We can see, hey, there's our stage. That looks great. The deal name is 100 apps, 100 hours contract. It's worth $1,000,000. So we'll add that. How many zeros do we have there? 1, 2, 3, 4, 5, 6. Alright. Let's make this for the Little League team. Great. My wife is the primary contact. Here's some notes on this deal. Great. Did we save it? Numeric value and deals is out of range. Uh-oh. Wouldn't be a 100 apps, a 100 hours without, some type of issue. Right? So let's take a look at this. The deal value, what did I set? Precision and scale. Maybe we bumped this up way up. Probably could have gone with integers for this as well. Do I have a minimum and a maximum? I I don't have that set. So not entirely sure what's going on. Let's just try this again. 100 apps, 100 hours contract, 1 100,000, 1,000,000. Try it again. Save. Okay. Alright. So now, we've got this particular deal set up. This is kind of an underwhelming view. Right? And, again, I might control how these things display. So if I just go into our deals, we go to the deal stage. If I wanna control how it displays, I can go in and set this. So let's do the color, do the icon, do the name, just because, I'm a I'm a very visual person, and I suspect, a lot of your end users may be as well. We still don't see the organization there. So, again, we'll go back. We'll adjust our organization. We wanna display related values. We want to show the name of that specific organization. And let's get even fancier and maybe we wanna show that logo. I'm gonna click on this and then you'll see this one that has a little magic beside it that's a thumbnail. That's just a shortcut for, like, a nice thumbnail sized image instead of loading the actual image size. So I donit have a logo for this company, but I could quickly find 1. Alright. So, we'll just copy this image address, go back to our instance, let's load up that specific company, they who shall not be named, Hit import. Oh, that's a data URL. Let's find an actual URL that we can just copy. Great. Let's try this again. We can import via URL. Cool. And now when I'm looking at my deals, I can see the logo of that organization. That makes it really handy, when I'm working on a deal just to have that extra extra visual reinforcement. Now, when I'm looking at deals, I may want to set up like a traditional pipeline type of view. So I could do that really easily just by switching the layout here inside Directus. So we'll change this to a Kanban layout. Let's group by the deal stage and the group title is gonna be the name. So I can see new assigned qualified demo. We have that Kanban view that we're used to. As far as the text, what do we wanna look at? We want to control, let's say, when this was created. I'm not really concerned about that. The tags, what do we want to set that to be? Do we want a card image? Honestly, this looks okay. Do we need an actual user on here? Probably not. Cool. Alright. We'll just keep it as is and let's let's start fleshing this out a little more as well. Right? So within a deal, when I'm working this deal, we're going to have activities assigned to this as it moves across the pipeline. And, for that, let's just go in and add a new table, a new collection, we're going to call it activities. Activities, I think that's the correct spelling. We use generated UID, we'll do created at, createdby, create up, updatedat, updated by. Great. And, in this case, maybe we do give a status for this particular activity. Right. Status, great. Let's give a name of the activity, probably a type of activity. Makes sense. We use a drop down for that. It seems pretty straightforward or, you know, maybe they're not gonna need to change the activity types on a a very frequent basis. So we'll call it the activity type, and let's add a couple choices to this. So this is a relatively new addition to Directus within the drop down, the ability to have an icon and a color. So let's say text is a phone call, value is a phone. One of the other cool things if you want to translate this value, you could use this phone t, or this dollar sign t for a translatable string. And then anytime you have users who are using the app within a different language, inside the settings, you can control all those custom translations. We'll take a look at that in a moment. Let's call this what? Phone call. The value could just be phone call, depending on how I wanna store this. Right? It may have an underscore within it as well. It could just be the same thing. Either way. Look for that phone. Great. And I could even add a color for that if needed. Maybe I just wanna keep these all the same. Phone call, we'll call it a meeting. And for this, do we have a meeting icon? Right? We calendar looks nice. Maybe we want to track demos separate from meetings. Alright. So we drag a demo. That was the TV icon that we had. Great. What else are we gonna need? Like, an email? Not sure I would Yeah. Maybe, like, a follow-up email. We wanna reschedule that. And then we'll do an email. Great. Demo, phone call. I think I'm gonna set this to be underscore value. Cool. Alright. Looking good. Activity type. Okay. What else are we going to need for this? We need a due date for this activity and the status, we can use as as is. It's published draft, archived. What do I really care about this activity? It probably isn't completed or not. Right? So maybe we scrap status, and maybe we just go for a toggle instead. Right? Hey. Is this completed? The default value, great. For our label, maybe we change it to completed and give it a color of color on. Color off is red, just to show that it has not been completed. Great. And let's just clean up this form a little bit, making sure everything looks nice. Okay. We are going to add a date for this, and let's set a specific date and time that this thing occurred. Call it due date, end date. Due date seems reasonable, but when do we actually need to complete this specific task? Okay. So now we've got an activity. We want to, link this to our actual deal so we can track those. Right? So, what I'm gonna do in this case is create a mini to one relationship because this activity could only belong to a single deal. Right? We're gonna use the key of deal. The related collection, we'll set that to deals. We'll show the name of that deal. Maybe we show the actual organization as well so I can actually dig into the related collections and and show values from that? Excuse me one second. Sorry about that. Cool. Alright. So now, what I forgot to do is create that inverse relationship. You can actually set that up via direct us when you're creating that relationship. But now I can also just go into our deals, not deal. We'll go into our deals, and now we're gonna create a one to many relationship back to those activities. So we'll call this key of activities. We've got activities. The foreign key will be deal. That already exists. Maybe we wanna show these in a table. We'll choose the columns, due date, Name, Due Date, Activity Type. Seems pretty savvy. And I can even filter these, right? So if I wanted to see just activities where they were not completed. We can enable search and filtering and show a link to these. We'll take a look at what all these look like in just a moment. But we've forgotten one important step through this whole process is, hey. We need somebody to assign these deals to. Right? So, let's add a many to one relationship. We'll call that the deal owner or, you know, you potentially say who this is assigned to. Like, the deal owners, again, kind of standard naming in these scenarios. And for the related collection here, we're gonna use directus underscore users. So these are gonna be actual users of the application that we're assigning this to. Invalid payload collections can't start with direct us users. Oh, deal owner. Let's go to our related collection, and let's get direct us underscore users. And, in this case, we're gonna show the first name, last name. We may back up and do an avatar as well. So just the the thumbnail, the avatar, I could move these around just by using edit raw value. You'll see these are just, the standard mustache syntax that you see throughout Directus as well. Excuse me. Let me get this a drink of water. I'm actually dying. That's a turn of fate. Okay. Alright. So we're gonna save this. Deal owner already exists in deals. Okay. Alright. Great. Let's move this around. Maybe we make deal stage half width. We slide deal owner up there. Let's actually take a look at this now. I should be able to assign folks. So let's create a new user. We'll just call it sales rep. Salesrep@example.com. And maybe we give them a nice avatar. Right? Sales rep, avatar. Let's just see what Google comes back with. John's inside sales rep. Yeah. This looks this is perfect. This is my guy right here. Alright. There's his avatar. We'll just save that. And now we can see who we've assigned this particular deal to. And now maybe within the deal card user here, I wanna show who that deal owner is. Great. Mister sales rep. Looking good. Alright. One of the other things that we need to do on our activities, we probably got a an owner of that activity or assigned to. It's been assigned to somebody on the team. Again, that's going to be assigned to a direct us user. We'll save that. And lots going on here. Just some type of weird glitch. I could see a couple of extra collections. I'll just remove these. Alright. Cool. So now weive got a deal. Weive got a table full of activities. I can go ahead and add these, like, follow-up on proposal. This is assigned to Mr. Sales Rep. We've got the activity type. We can see that's going to be, just a quick phone call. This is completed. We can see that conditional, conditional formatting for that. And we can add a due date of, let's say, the next Friday. Great. Save that. Keep editing. Cool. Alright. So, now, we've got the basic inner workings of a CRM. Right? We've got our deals, we've got contacts, we've got organizations, we've got our different pipeline stages. Right? If I wanted to organize these things a bit, we could go in and add different icons for each of these. So, you know, maybe we set some people icons for our contacts. We've got our organizations. Do we have an organization option? Let's look and see. Business. Is there a business? There you go. That looks somewhat like a business. We've got activities. Maybe this will be like a checklist. Cool. We've got our deals. Let's make those the money. Dollar signs, that's great. And then, deal stages, to me this is like a settings. Right? So I could create a new folder, let's just call it settings folder. You don't necessarily have to add this, but maybe we just do to keep it clean. And we look for, like, a settings icon just to use here. That's great. This one looks magical. Settings suggest. Right? And, again, I can change the name of this to just say settings. So, it still creates this collection, but, we can call it whatever we want. So we'll drag this up within settings folder. We'll drag deal stages and, this will be, what, like the Kanban view. There we go. Awesome. Okay. So now we get a little more organization to this. One of the other things that you might do and, you know, that you use all the time within ACRM are saved views. Right? So Directus gives you that ability with bookmarks. We'll just go in to the top here and maybe I want to sort by a specific sales rep. Right? Like, the deal owner is, a specific person and specific name is sales rep. That's the only one at this point. But I could go in and create a bookmark for this, and we could call it, deals sales rep Man. And we can change this up, give it a color. Now, within that collection, now we can see we've got our deals for Sales Rep Van, and I could save that bookmark. So even if I go into the main deals view, and maybe we change this back to a table view, which could be easier for, you know, maybe a sales manager or something who's controlling this. So I had deal owner back to this as well. Now if I go back, deal sales rep man view, boom, there it is. It's saved. I could go in and update this if I wanted to as well. So now we've got our pipeline. When we go into each one of the deal, we have our name, we've got our organization, we've got the notes, we've got our activities. You know, we can mark these activities off as completed. That seems like a great CRM baseline. Let's let's take a look at where we're at. Right? We got, like, 16 minutes on the clock. This feels like a win. I don't I don't know if we wanna run that one, let's discuss where we could go from here, right, maybe we want to automatically send some emails when it hits a certain stage in the pipeline. So let's just call these things done. Right? We can manage all of our contacts. We can manage all of our organizations. We can manage our sales pipelines. We can track our activities and follow-up. So let's say, you know, we get a new deal inbound. Maybe we want to automatically assign that to a a particular person, or we want to send a notification to our sales rep when that assignment happens. Let's figure out how to do that. Right? So I'm gonna go in. Let's just create a new deal. We'll say actually, let's wait a moment. Let's go into our flows. This is a good example. Right? Whenever a a new deal comes inbound, we want to send an email notification to our sales rep to to let them know. Alright. So we're gonna create a new flow. We'll just call it new inbound deal. Pretty straightforward. We could change this to the new symbol if we want to and just do a trigger setup. So what are we going to choose here? Directus gives you a ton of different options, as far as what to use when you're creating a flow. In this case, we're going to use the event hook. So when a certain event happens inside the platform, we wanna trigger an automation. The type that we're gonna choose here is action non blocking because we don't, the the filter allows you to basically either adjust the payload when a new deal gets created or a new event happens. Action non blocking, again, that runs after a create or update action. So in this case, let's do the items dot create. Whenever we're gonna trigger this based on the deals collection. And cool. Alright, so now I'm just going to save this, right? I'm going to go in and let's create a new deal inside the system. It's in the new stage. We're gonna assign this to mister sales rep. New deal automation. And we add, let's set this one to be for the little league. Again, we choose a specific contact, add some notes, and, maybe, the deal value, we'll just ballpark it at $5,000. Great. If I go back, now I can see that in my logs, I've got this flow. Here's the payload of this particular flow, and we could see who the deal owner is on this particular deal. How do we send an email notification to that specific owner? Right? I'm just gonna take this, copy it, and I open up just my Versus code editor where I've got my 100 apps, just, Docker Compose file here set up to run this locally. I'm just going to save that in case I need it. And, next, let's flesh this out a little bit. Right? So you can see the data we're getting back here. We're probably gonna need to look up that specific user. We can find them there. Those are the deal owner. We could send them a notification inside the app or we could send them a notification via email. Right? There's 2 options there. We've got notification. In this case, we've got the UUID of the user that we're going to send that to. So, you know, potentially, you want to send that in app. App. In this case, if there's a new deal, they're probably gonna be in their inbox. We're going to send that new deal to them. But let's actually find that email address first, though. Alright. So we're gonna read data from a specific collection. Let's call it Find User is the actual step we're gonna do here. For the permissions, let's just give full access. And, for the collection, you can see I don't have the Directus Users collection here, but I can go in and edit my raw value and just use Directus underscore users. And for our IDs, right, what are we gonna put here? So if I open this back up, we're gonna use the trigger. Payload. Dealowner. So we'll do this, we'll do trigger dot payload dot deal underscore owner. And I'm gonna wrap this in mustache syntax. And let's try this again. So we'll read the user. Let's go ahead and maybe just add this send email as well. So we'll send the email, and this is gonna be the read underscore user. We're using the key that we set of the previous operation within that flow. Readuser. Email should be. We'll input that. New deal assigned to you. And hey. Read_user.firstname. We've assigned a new deal to you. And then I can even go through and add those different variables if I wanted to for things like the deal name or the deal ID and and add a link back to that. So let's just try trigger. Payload.deal_name. Great. We'll save it. And now let's just test this out. I'm not actually sure if I've got emails set up here locally on this particular instance though, so that could be fun. But we should be able to actually see if this runs. In light of that bit of news, because it just looking at my configuration here, I do not have email configured here locally. So let's let's detach that one, and let's just test the notifications. Send notification. Cool. Send notification. The find_user.id. Cool. Full access. And it was a new deal assigned. We'll just add the key here. So that'll be trigger dot payload. Or no. Actually, it may be something like this where we have key. Alright. Let's just take a look just to make sure. Alright. Within our payload, we can see the key there. That's good. In that case, we probably didn't need to get the actual user there, but that's okay. I'm just gonna copy this message that we set here. Just paste that. And let's take a look at where this gets us. Alright. So we've got a new inbound deal. We're going to read the data of the user that we've assigned that to and we're going to send a notification to that specific person. Alright. Now we've got a new deal, deal stage. Let's just set it to new. Deal owner. I'm gonna choose myself here so we could test this notification. Say deal name. Test deal. It's worth $6,000,000. Very nice deal. We got a primary contact. Here's some notes. We'll just save that and let's see what happens. Right? We go to our Flow, we get a new inbound deal, we can see our logs, send notification, recipient is so and so. Hey, undefined. We've sent you a new test deal. Underscore first name. Why did that not come back? Read underscore oh, that's why I used the wrong key. Sometimes that happens. Find user dot first name. Great. So now, if I look and I check my activity log, I can see that I've got this new deal signed. Here's the notification for that. And if I click on it where it says view content, it should take me to that specific test deal. Great. Awesome. Let's call that a win. Right? We've got our custom CRM built out. We've set up some automation for this. We could go further and flesh this out a a ton of different ways. Right? If we talk about it, like, we could go through and send emails to our actual primary contact when it reaches a certain stage, that would be fairly easy to do using direct dis flows. You know, we could maybe even go as far as, like, a future future iteration of this could be setting up an inbox to parse incoming emails like a BCC functionality and add these to those specific deals as well. But this this feels really good. I'm I'm calling this a win. That's our custom CRM. Thanks for joining me on this episode of 100 apps, 100 hours. We'll catch you on the next one.","592c22ad-1cb2-4d65-9bd7-e0d7c98fe4f2",[185],"e9e66fa8-0650-4e37-ae8b-74755fdd5dca",[],{"reps":188},[189,245],{"name":190,"sdr":8,"link":191,"countries":192,"states":194},"John Daniels","https://meet.directus.io/meetings/john2144/john-contact-form-meeting",[193],"United States",[195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244],"Michigan","Indiana","Ohio","West Virginia","Kentucky","Virginia","Tennessee","North Carolina","South Carolina","Georgia","Florida","Alabama","Mississippi","New York","MI","IN","OH","WV","KY","VA","TN","NC","SC","GA","FL","AL","MS","NY","Connecticut","CT","Delaware","DE","Maine","ME","Maryland","MD","Massachusetts","MA","New Hampshire","NH","New Jersey","NJ","Pennsylvania","PA","Rhode Island","RI","Vermont","VT","Washington DC","DC",{"name":246,"link":247,"countries":248},"Michelle Riber","https://meetings.hubspot.com/mriber",[249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,226,437,438],"Albania","ALB","Algeria","DZA","Andorra","AND","Angola","AGO","Austria","AUT","Belgium","BEL","Benin","BEN","Bosnia and Herzegovina","BIH","Botswana","BWA","Bulgaria","BGR","Burkina Faso","BFA","Burundi","BDI","Cameroon","CMR","Cape Verde","CPV","Central African Republic","CAF","Chad","TCD","Comoros","COM","Côte d'Ivoire","CIV","Croatia","HRV","Czech Republic","CZE","Democratic Republic of Congo","COD","Denmark","DNK","Djibouti","DJI","Egypt","EGY","Equatorial Guinea","GNQ","Eritrea","ERI","Estonia","EST","Eswatini","SWZ","Ethiopia","ETH","Finland","FIN","France","FRA","Gabon","GAB","Gambia","GMB","Ghana","GHA","Greece","GRC","Guinea","GIN","Guinea-Bissau","GNB","Hungary","HUN","Iceland","ISL","Ireland","IRL","Italy","ITA","Kenya","KEN","Latvia","LVA","Lesotho","LSO","Liberia","LBR","Libya","LBY","Liechtenstein","LIE","Lithuania","LTU","Luxembourg","LUX","Madagascar","MDG","Malawi","MWI","Mali","MLI","Malta","MLT","Mauritania","MRT","Mauritius","MUS","Moldova","MDA","Monaco","MCO","Montenegro","MNE","Morocco","MAR","Mozambique","MOZ","Namibia","NAM","Niger","NER","Nigeria","NGA","North Macedonia","MKD","Norway","NOR","Poland","POL","Portugal","PRT","Republic of Congo","COG","Romania","ROU","Rwanda","RWA","San Marino","SMR","São Tomé and Príncipe","STP","Senegal","SEN","Serbia","SRB","Seychelles","SYC","Sierra Leone","SLE","Slovakia","SVK","Slovenia","SVN","Somalia","SOM","South Africa","ZAF","South Sudan","SSD","Spain","ESP","Sudan","SDN","Sweden","SWE","Tanzania","TZA","Togo","TGO","Tunisia","TUN","Uganda","UGA","United Kingdom","GBR","Vatican City","VAT","Zambia","ZMB","Zimbabwe","ZWE","UK","Germany","Netherlands","Switzerland","CH","NL",1773850444441]