Lista de espera para um novo SaaS

Lista de espera para um novo SaaS8 min de leitura

Como montar um formulário de inscrição para beta que faz triagem dos candidatos

Um formulário de candidatura ao beta com perguntas de triagem que o servidor exige, e um script que pontua os candidatos e escolhe cada turma da lista.

Preet SutharFundador, Ikiform

Faça algumas perguntas de triagem junto do e-mail e deixe o backend de formulários exigi-las, para que toda candidatura chegue completa e comparável. Depois escolha cada turma da lista com um script curto de pontuação, convide um grupo pequeno e deixe todos os outros esperando a próxima rodada.

Uma lista de espera manda e-mail para todo mundo no dia do lançamento. Um beta faz o contrário: você deixa entrar vinte pessoas, aprende com elas e depois deixa entrar mais vinte. Isso muda o formulário. Você precisa de respostas pelas quais dê para ordenar, não só de um endereço.

O que muda num beta

  • É uma seleção. Você convida poucas pessoas por vez, então precisa de um jeito de comparar os candidatos entre si. Escolhas fechadas, como função e tamanho da equipe, ordenam bem. Texto livre não.
  • Candidaturas incompletas custam uma vaga. Se alguém pula a pergunta que diz se a pessoa se encaixa, você não consegue escolhê-la nem descartá-la. Torne as perguntas de triagem obrigatórias e faça o servidor recusar envios que as pulem.
  • Os convites saem em rodadas. Sua lista precisa lembrar quem você já convidou, para a próxima turma pular essas pessoas. O Ikiform não tem tags, então esse estado fica no seu script ou na sua planilha.
  • Quem não foi escolhido ainda conta. Essas pessoas são a sua próxima turma. Diga na frase de agradecimento que os grupos são chamados ao longo do tempo, para que uma caixa de entrada silenciosa não seja lida como um não.

Perguntas de triagem

Seis perguntas é o teto. Cada uma deve mudar quem você convida; se você não sabe dizer como uma resposta faria alguém subir ou descer, corte. Experimente o formulário abaixo. Ele roda a mesma definição de campos que o Ikiform aplica no servidor, então você vê o 422 que uma candidatura incompleta recebe.

Prévia

Roda o validador do Ikiform no seu navegador. Nada é enviado.

Tamanho da equipe

Uma ou duas frases sobre a tarefa que você passaria para ele no primeiro dia.

Cole esta definição na aba Fields do formulário. A resposta use_case precisa de pelo menos 40 caracteres, o que filtra candidaturas de uma palavra só.
CampoO que ela decidePor que este tipo
emailQuem convidarObrigatório. Um e-mail de trabalho dá uma pista da empresa sem perguntar
roleSe a pessoa sente o problema que você resolveUm select com as suas próprias faixas, para você poder dar pesos
team_sizeSe a sua primeira versão serve para o cenário delaUm radio: uma resposta, sempre comparável
use_caseSe a pessoa vai usar o beta este mêsUm textarea com tamanho mínimo. O sinal mais honesto do formulário
current_toolQuem você está substituindo, e com o que vão comparar vocêTexto livre opcional
interviewCom quem você pode conversarUm checkbox. Ausente significa false, então ele nunca bloqueia uma candidatura
Com uma definição de campos, o Ikiform recusa qualquer campo que não conhece com 422 e um erro unknown_field. Se você adicionar um input oculto como ref, adicione-o também à definição.

O formulário

Todas as versões abaixo mostram a mensagem do servidor abaixo do campo a que ela se refere, porque um formulário de triagem falha por mais motivos do que um e-mail inválido. Cada uma mantém o que a pessoa digitou quando um envio é recusado, para ninguém redigitar um parágrafo.

O required e o minlength nativos pegam a maioria das lacunas antes do envio. O formulário simples funciona sem script e cai na sua página de agradecimento; o script mantém as pessoas na página e lista cada erro de campo.

beta.html
<form id="beta-form" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required>

  <label for="role">Your role</label>
  <select id="role" name="role" required>
    <option value="">Choose one</option>
    <option value="founder">Founder</option>
    <option value="engineer">Engineer</option>
    <option value="product">Product manager</option>
    <option value="designer">Designer</option>
    <option value="other">Something else</option>
  </select>

  <fieldset>
    <legend>Team size</legend>
    <label><input type="radio" name="team_size" value="1" required> Only me</label>
    <label><input type="radio" name="team_size" value="2-10"> 2 to 10</label>
    <label><input type="radio" name="team_size" value="11-50"> 11 to 50</label>
    <label><input type="radio" name="team_size" value="51+"> More than 50</label>
  </fieldset>

  <label for="use_case">What would you use it for first?</label>
  <textarea id="use_case" name="use_case" rows="4" minlength="40" maxlength="1000" required></textarea>

  <label for="current_tool">What do you use today?</label>
  <input id="current_tool" name="current_tool" maxlength="120">

  <label><input type="checkbox" name="interview" value="true"> I'm open to a 20-minute call</label>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden>
  <ul id="beta-errors" role="alert"></ul>
  <button>Apply for the beta</button>
</form>
Antes de </body>
<script>
  const form = document.getElementById("beta-form");
  const errors = document.getElementById("beta-errors");
  const labels = { email: "Work email", role: "Role", team_size: "Team size", use_case: "First use", current_tool: "Current tool" };

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const button = form.querySelector("button");
    button.disabled = true;
    errors.replaceChildren();
    try {
      const response = await fetch(form.action, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(form),
      });
      const result = await response.json();
      if (result.ok) {
        form.outerHTML = "<p role=\"status\">Thanks for applying. We invite testers in small groups and will email you either way.</p>";
        return;
      }
      const fields = Object.entries(result.error.fields ?? {});
      const lines = fields.length ? fields.map(([id, messages]) => `${labels[id] ?? id}: ${messages[0]}`) : [result.error.message];
      errors.replaceChildren(...lines.map((line) => Object.assign(document.createElement("li"), { textContent: line })));
    } catch {
      errors.textContent = "Couldn't reach the server. Your answers are still here; try again.";
    }
    button.disabled = false;
  });
</script>
As versões no servidor enviam com uma chave secreta, então o Ikiform pula Allowed domains e a verificação humana. Todo envio passa a vir do IP do seu servidor, e o limite Standard de 5 por minuto por visitante conta todos os seus visitantes juntos. Escolha o limite de taxa Relaxed ou Custom para um formulário que você envia por um servidor, e mantenha IKIFORM_SECRET_KEY em variáveis de ambiente só do servidor.

Escolha uma turma

Como as candidaturas viram turmas do betacandidaturascompletas,exigidaspeloservidorpontuaçãofunção,tamanho,esforço,conversatop 25turma 1,convidadapróximarodadapular quemfoiconvidadoaindaesperandoavisados queos grupossaem aolongo dotempoexportarduas semanasabaixo do corteComo as candidaturas viram turmas do betacandidaturascompletas, exigidaspelo servidorpontuaçãofunção, tamanho,esforço, conversatop 25turma 1, convidadapróxima rodadapular quem foiconvidadoaindaesperandoavisados queos grupossaem aolongo dotempoexportarduas semanasabaixo do corte
  1. As candidaturas chegam completas, porque o Ikiform recusa envios que pulam uma pergunta obrigatória.
  2. Um script as lê pela API e dá a cada uma uma pontuação por função, tamanho da equipe, tamanho do caso de uso e o checkbox da conversa.
  3. As 25 primeiras viram a primeira turma e recebem um convite pela sua ferramenta de e-mail.
  4. Duas semanas depois, rode o script de novo com os e-mails convidados, para que sejam pulados, e escolha o próximo grupo.
  5. Todos abaixo do corte continuam na lista e têm notícias suas quando chegar a rodada deles.
As pontuações são uma ordem inicial, não um veredito. Leia você mesmo o caso de uso de todos que estão perto da linha de corte.

O script lê todas as candidaturas recebidas com a API REST, mantém a mais nova por e-mail, dá uma pontuação e grava o grupo do topo em cohort.csv. Passe os e-mails que você já convidou como argumentos para a próxima rodada pulá-los. Ele precisa de uma chave secreta com o escopo submissions:read.

cohort.mjs
import { writeFileSync } from "node:fs";
import { createClient } from "@ikiform/sdk";

const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY });
const COHORT_SIZE = 25;
const invited = new Set(process.argv.slice(2).map((email) => email.toLowerCase()));

const ROLE = { founder: 3, engineer: 3, product: 2, designer: 1, other: 0 };
const SIZE = { "2-10": 3, "11-50": 2, "1": 1, "51+": 1 };

const people = new Map();
for await (const submission of ikiform.submissions.iterate({ formId: "frm_xxxxxxxxxxxxxxxx", status: "received", limit: 100 })) {
  const answers = submission.fields;
  const email = String(answers.email ?? "").trim().toLowerCase();
  if (!email || invited.has(email) || people.has(email)) continue;
  const words = String(answers.use_case ?? "").split(/\s+/).length;
  const score =
    (ROLE[answers.role] ?? 0) +
    (SIZE[answers.team_size] ?? 0) +
    Math.min(3, Math.floor(words / 25)) +
    (answers.interview === true ? 2 : 0) +
    (answers.current_tool ? 1 : 0);
  people.set(email, { email, score, role: answers.role, team_size: answers.team_size });
}

const ranked = [...people.values()].sort((a, b) => b.score - a.score);
const cohort = ranked.slice(0, COHORT_SIZE);
const quote = (value) => `"${String(value ?? "").replaceAll('"', '""')}"`;
const rows = cohort.map((person) => [person.email, person.score, person.role, person.team_size].map(quote).join(","));
writeFileSync("cohort.csv", ["email,score,role,team_size", ...rows].join("\n"));
console.log(`${ranked.length} applicants, ${cohort.length} in this cohort, lowest score ${cohort.at(-1)?.score}`);
  • Mude ROLE e SIZE de acordo com o público da sua primeira versão. Uma ferramenta para equipes pode dar a maior pontuação a 2-10; uma ferramenta individual, a 1.
  • O caso de uso ganha um ponto a cada 25 palavras, até 3. Tamanho não é qualidade, mas quem escreve três frases costuma aparecer.
  • Colocar cada valor entre aspas evita que vírgulas nas respostas quebrem o CSV.
  • Guarde a lista de convidados num lugar fácil de achar, como um invited.txt que você passa com $(cat invited.txt).

Envie o convite

Importe o cohort.csv na sua ferramenta de e-mail e envie o convite de lá. O Ikiform não envia e-mail para quem preenche o formulário. Os e-mails de notificação vão só para você e sua equipe, então não há resposta automática, e-mail de confirmação nem double opt-in. Envie esses e-mails pela sua ferramenta de e-mail, disparados por um webhook. Mantenha o primeiro convite curto: o que a pessoa ganha, a única coisa que você quer que ela teste e como falar com você. Cada e-mail de notificação que o Ikiform manda para você tem um Reply que vai para o candidato, o que serve bem para uma mensagem pessoal rápida a quem está perto da linha de corte.

Confira se funciona

  1. Em Allowed domains do formulário, adicione o domínio do seu site, mais localhost enquanto desenvolve. Deixe vazio para aceitar envios de qualquer lugar.
  2. Cole a definição de campos na aba Fields do formulário e salve.
  3. Envie uma candidatura com um caso de uso de uma linha. O formulário mostra a mensagem de tamanho mínimo abaixo desse campo, e nada é armazenado.
  4. Envie uma candidatura completa. Ela aparece em Submissions com todas as respostas, e interview mostra true ou false.
  5. Confira a caixa de entrada de cada destinatário das notificações. Cada um recebe um e-mail com os campos, e Reply vai para quem enviou quando o formulário tem um campo email.
  6. Envie com o honeypot preenchido, _gotcha=bot. Ele ainda recebe uma resposta de sucesso, mas o envio é armazenado como spam e não dispara e-mail nem webhook.
  7. Rode o script de pontuação com uma chave secreta e confira se o cohort.csv lista a sua candidatura de teste com a pontuação dela.

Pegadinhas

ProblemaO que fazer
422 validation_failedO envio não corresponde à definição de campos do formulário. error.fields aponta cada campo e o motivo. Faça o name do input bater com o id do campo.
422 com Unknown field para ref, utm_source ou outro input ocultoA definição de campos não o lista. Adicione-o como um campo text opcional, ou renomeie-o com um _ no início para o Ikiform descartá-lo.
Uma resposta longa some depois de um erro no Next.jsO React reinicia o formulário depois de uma server action. Devolva as respostas a partir da action e defina defaultValue em cada input, como o exemplo de Next.js faz.
O script de pontuação lança 401 unauthorized ou 403 forbiddenUm 401 significa que a chave está ausente ou foi revogada; um 403 significa que ela é publicável ou não tem o escopo. Use uma chave secreta (ik_sk_) com o escopo submissions:read.
O envio está no painel, mas nenhum e-mail ou webhook chegouEle foi armazenado como spam. Spam não envia nada. Abra-o no filtro Spam para ver os motivos e marque-o como não spam para enviar o e-mail e os webhooks dele.

Perguntas

Travou em algum passo ou quer um guia para o seu caso? Envie um e-mail para hello@ikiform.com.