The power of automation: How I built the Ship30 Endless Idea Generator into Notion using AI and Go to generate article ideas directly within Notion

The power of automation: How I built the Ship30 Endless Idea Generator into Notion using AI and Go to generate article ideas directly within Notion

One fine Saturday evening, I was generating article ideas using the ChatGPT prompt provided by Ship30 and thought to myself “This would be so much easier to do directly in Notion”

At that moment I went down a rabbit hole figuring out how I could do this. I’ve built plenty of ancillary tools into my Notion setup so I was comfortable with that end, but I’d never used the OpenAI API. Thirty minutes and a proof of concept later, I realized I could totally pull this off!

Here are the steps I took to build the Ship30 EIG into my Notion workspace.

Create the OpenAI client in Go

The first thing I did was create an API key at https://platform.openai.com/account/api-keys.

This key is used within the code to authenticate with my OpenAI account. I used the github.com/sashabaranov/go-openai package that was set up for exactly what I needed. Finally, using the Endless Idea Generator ChatGPT (stored as a .txt file) prompt with the ChatMessageRoleSystem role, I can train the bot to accept a generic prompt.

This is the function that puts that whole thing together:

func GenerateIdeas(prompt string) (*string, *string, error) {
    f, err := os.ReadFile("./eig.txt")
    if err != nil {
        return nil, nil, err
    }
    eigPrompt := string(f)

    client := openai.NewClient(key)
    resp, err := client.CreateChatCompletion(
        context.Background(),
        openai.ChatCompletionRequest{
            Model: openai.GPT3Dot5Turbo,
            Messages: []openai.ChatCompletionMessage{
                {
                    Role:    openai.ChatMessageRoleSystem,
                    Content: eigPrompt,
                },
                {
                    Role:    openai.ChatMessageRoleAssistant,
                    Content: prompt,
                },
            },
        },
    )

    if err != nil {
        return nil, nil, err
    }
    return &resp.ID, &resp.Choices[0].Message.Content, nil
}

Notion configuration

I have two databases in Notion for this project: EIGRequests and EIGIdeas.

EIGRequests is where I can enter a prompt for ideas I want to generate. The title field is the actual prompt, although I can manually specify the topic, audience, and outcome if needed. The prompt is actually modified to generate a JSON payload instead of text so I can easily parse it.

This is what the database looks like:

EIGIdeas holds the generated ideas.

Once the list of ideas is generated in that JSON structure, I can parse them into this table. The table is broken down by idea, idea type, and idea subtype. I also have a relation field linking back to the original prompt so I know where it came from.

Here is the EIGIdeas database:

Running it on my server

I want this thing to work without having to manually run it, so I decided to use the github.com/go-co-op/gocron package to do so.

This package allows me to easily specify an interval to run any function in Go. I run it on my home server with Kubernetes, but you could easily set this up in an EC2 instance. When the function runs every 3 min, it does the following:

  • Check the EIGRequest database for open requests.

  • Flip the status of the request to Pending.

  • Parse the response and send it to OpenAI using the function shown above.

  • Take that response and create a record in the EIGIdeas database for each one.

  • Put the raw JSON into the specific page with the request.

The end result is that I can add a prompt to Notion, and this system will generate 29 article ideas for me.

Here is how the go-cron package is configured to run the code every 3 minutes:

s := gocron.NewScheduler(time.UTC)
s.Every(3).Minute().Do(CheckForRequests)

Here is the function called by go-cron:

func CheckForRequests() {
    // Call the Notion API to get any EIGRequest records with "Ready" status
    openRequests, err := GetOpenRequests()
    if err != nil {
        log.Fatal(err)
    }

    for _, el := range openRequests.Results {
        props := el.Properties.(notion.DatabasePageProperties)
        prompt := props["Prompt"].Title[0].Text.Content

        // Call the Notion API to change the status to Pending so it doesnt get called again
        err = SetStatusToPending(el.ID)
        if err != nil {
            log.Fatal(err)
        }

        var id *string
        var response *string

        // Some retry logic in case something doesnt go right 
        ok := false
        i := 0
        maxTries := 5
        for i < maxTries {
            i++
            id, response, err = GenerateIdeas(prompt)
            if err != nil {
                log.Fatal(err)
            }

      // Response is probably good since its JSON (Starts with '{')
            if strings.HasPrefix(*response, "{") {
                i = maxTries + 1
                ok = true
            }
        }
        if !ok {
            // If I don't get a good response, set the EIGRequest to Error status for later
            err = CompleteEigRequest(el.ID, *id, *response, "Error", nil)
            if err != nil {
                log.Fatal(err)
            }
            return
        }

        var pr ParsedResponse
        err = json.Unmarshal([]byte(*response), &pr)
        if err != nil {
            log.Fatal(err)
        }

        // Loop over the response from OpenAI and create an EIGIdea for each
        for ideaType, subTypeStruct := range pr.Ideas {
            for subtype, idea := range subTypeStruct {
                err = CreateEigIdea(el.ID, ideaType, subtype, idea)
                if err != nil {
                    log.Fatal(err)
                }
                time.Sleep(2 * time.Second)

            }
        }

        // Change EIGRequest to Done, add response to body
        err = CompleteEigRequest(el.ID, *id, *response, "Done", &pr)
        if err != nil {
            log.Fatal(err)
        }
    }
}

Finally just to give an idea, here are what many of the functions to call Notion look like:

var c *notion.Client

func SetStatusToPending(pageId string) error {
    _, err := c.UpdatePage(context.Background(), pageId, notion.UpdatePageParams{
        DatabasePageProperties: notion.DatabasePageProperties{
            "Status": notion.DatabasePageProperty{
                Status: &notion.SelectOptions{
                    Name: "Pending",
                },
            },
        },
    })
    return err
}