Lista de espera para un nuevo SaaS

Lista de espera para un nuevo SaaS8 min de lectura

Cómo crear un formulario de registro para una beta que filtre a los candidatos

Un formulario de solicitud para una beta con preguntas de filtro que aplica el servidor, y un script que puntúa a los candidatos y elige cada grupo de la lista.

Preet SutharFundador, Ikiform

Haz unas pocas preguntas de filtro junto al email y deja que el backend de formularios las haga cumplir, para que cada solicitud llegue completa y comparable. Luego elige cada grupo de la lista con un breve script de puntuación, invita a un grupo pequeño y deja a los demás esperando la siguiente ronda.

Una lista de espera escribe a todos el día del lanzamiento. Una beta hace lo contrario: dejas entrar a veinte personas, aprendes de ellas y luego dejas entrar a veinte más. Eso cambia el formulario. Necesitas respuestas por las que puedas ordenar, no solo una dirección.

Qué cambia en una beta

  • Es una selección. Invitas a unas pocas personas cada vez, así que necesitas una forma de comparar a los candidatos entre sí. Las opciones cerradas, como el rol y el tamaño del equipo, se ordenan con facilidad. El texto libre no.
  • Las solicitudes incompletas cuestan una plaza. Si alguien se salta la pregunta que te dice si encaja, no puedes elegirlo y tampoco puedes descartarlo. Haz obligatorias las preguntas de filtro, y que el servidor rechace los envíos que las omitan.
  • Las invitaciones salen por rondas. Tu lista tiene que recordar a quién ya invitaste, para que el siguiente grupo lo omita. Ikiform no tiene etiquetas, así que ese estado vive en tu script o tu hoja de cálculo.
  • Quienes no fueron elegidos siguen contando. Son tu siguiente grupo. Diles en la línea de agradecimiento que los grupos salen con el tiempo, para que una bandeja de entrada en silencio no parezca un no.

Preguntas de filtro

Seis preguntas es el máximo. Cada una debería cambiar a quién invitas; si no sabes decir cómo una respuesta subiría o bajaría a alguien, quítala. Prueba el formulario de abajo. Ejecuta la misma definición de campos que Ikiform aplica en el servidor, así que puedes ver el 422 que recibe una solicitud incompleta.

Vista previa

Ejecuta el validador de Ikiform en tu navegador. No se envía nada.

Tamaño del equipo

Una o dos frases sobre la tarea que le darías el primer día.

Pega esta definición en la pestaña Fields del formulario. La respuesta use_case necesita al menos 40 caracteres, lo que filtra las solicitudes de una sola palabra.
CampoQué decidePor qué este tipo
emailA quién invitarObligatorio. Un email del trabajo da pistas sobre la empresa sin preguntar
roleSi sufren el problema que resuelvesUn select con tus propias categorías, para poder ponderarlas
team_sizeSi tu primera versión encaja con su entornoUn radio: una sola respuesta, siempre comparable
use_caseSi usarán la beta este mesUn textarea con longitud mínima. La señal más sincera del formulario
current_toolA quién sustituyes y con qué te van a compararTexto libre opcional
interviewCon quién puedes hablarUn checkbox. Si falta, vale false, así que nunca bloquea una solicitud
Con una definición de campos, Ikiform rechaza cualquier campo que no conozca con 422 y un error unknown_field. Si añades un input oculto como ref, añádelo también a la definición.

El formulario

Todas las versiones de abajo muestran el mensaje del servidor bajo el campo al que corresponde, porque un formulario de filtro falla por más motivos que un email incorrecto. Cada una conserva lo que escribió la persona cuando se rechaza un envío, así que nadie vuelve a escribir un párrafo.

Los atributos nativos required y minlength detectan la mayoría de los huecos antes del envío. El formulario sencillo funciona sin script y llega a tu página de agradecimiento; el script mantiene a la gente en la página y enumera cada error 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>
Las versiones de servidor envían con una clave secreta, así que Ikiform se salta Allowed domains y su verificación humana. Todos los envíos llegan entonces desde la IP de tu servidor, y el límite Standard de 5 por minuto por visitante cuenta a todos tus visitantes juntos. Elige el límite de frecuencia Relaxed o Custom para un formulario que envías a través de un servidor, y guarda IKIFORM_SECRET_KEY en variables de entorno solo de servidor.

Elige un grupo

Cómo las solicitudes se convierten en grupos de la betasolicitudescompletas,por elservidorpuntuaciónrol, tamaño,esfuerzo,llamadatop 25grupo 1,invitadosiguienterondaomitirinvitadosen esperasaben queirán porgruposexportardos semanasbajo el corteCómo las solicitudes se convierten en grupos de la betasolicitudescompletas, por elservidorpuntuaciónrol, tamaño, esfuerzo,llamadatop 25grupo 1, invitadosiguiente rondaomitir invitadosen esperasaben queirán porgruposexportardos semanasbajo el corte
  1. Las solicitudes llegan completas, porque Ikiform rechaza los envíos que se saltan una pregunta obligatoria.
  2. Un script las lee con la API y puntúa cada una según el rol, el tamaño del equipo, la longitud del caso de uso y la casilla de la llamada.
  3. Los 25 primeros forman el primer grupo y reciben una invitación desde tu herramienta de email.
  4. Dos semanas después, vuelve a ejecutar el script con los emails invitados, para que se omitan, y elige el siguiente grupo.
  5. Todos los que quedan por debajo del corte siguen en la lista y sabrán de ti cuando llegue su ronda.
Las puntuaciones son un orden de partida, no un veredicto. Lee tú mismo el caso de uso de todos los que estén cerca de la línea de corte.

El script lee cada solicitud recibida con la API REST, conserva la más reciente por email, la puntúa y escribe el grupo de cabeza en cohort.csv. Pasa como argumentos los emails que ya invitaste para que la siguiente ronda los omita. Necesita una clave secreta con el permiso 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}`);
  • Cambia ROLE y SIZE según para quién sea tu primera versión. Una herramienta para equipos podría puntuar más alto 2-10; una herramienta individual, 1.
  • El caso de uso suma un punto por cada 25 palabras, hasta 3. La longitud no es calidad, pero quien escribe tres frases suele presentarse.
  • Poner comillas a cada valor evita que las comas de las respuestas rompan el CSV.
  • Guarda la lista de invitados en un sitio donde la encuentres, como un invited.txt que pases con $(cat invited.txt).

Envía la invitación

Importa cohort.csv en tu herramienta de email y envía la invitación desde allí. Ikiform no envía emails a la persona que rellena el formulario. Los emails de notificación van solo a ti y a tu equipo, así que no hay respuesta automática, email de confirmación ni doble opt-in. Envíalos desde tu herramienta de email, activados por un webhook. Que la primera invitación sea breve: qué reciben, la única cosa que quieres que prueben y cómo contactarte. Cada email de notificación que te envía Ikiform tiene un Reply que va al candidato, ideal para una nota personal rápida a quienes están cerca de la línea de corte.

Comprueba que funciona

  1. En Allowed domains del formulario, añade el dominio de tu sitio, y localhost mientras desarrollas. Déjalo vacío para aceptar envíos desde cualquier sitio.
  2. Pega la definición de campos en la pestaña Fields del formulario y guárdala.
  3. Envía una solicitud con un caso de uso de una línea. El formulario muestra el mensaje de longitud mínima bajo ese campo y no se guarda nada.
  4. Envía una solicitud completa. Aparece en Submissions con todas las respuestas, e interview vale true o false.
  5. Revisa la bandeja de entrada de cada destinatario de las notificaciones. Cada uno recibe un email con los campos, y Reply responde a quien envió el formulario cuando este tiene un campo email.
  6. Envía con el honeypot relleno, _gotcha=bot. Sigue recibiendo una respuesta de éxito, pero el envío se guarda como spam y no manda email ni webhook.
  7. Ejecuta el script de puntuación con una clave secreta y comprueba que cohort.csv incluye tu solicitud de prueba con su puntuación.

Problemas habituales

ProblemaQué hacer
422 validation_failedEl envío no coincide con la definición de campos del formulario. error.fields indica cada campo y el motivo. Haz coincidir el name del input con el id del campo.
422 con Unknown field para ref, utm_source u otro input ocultoLa definición de campos no lo incluye. Añádelo como campo text opcional, o renómbralo con un _ delante para que Ikiform lo descarte.
Una respuesta larga desaparece tras un error en Next.jsReact reinicia el formulario después de una server action. Devuelve las respuestas desde la action y pon defaultValue en cada input, como hace el ejemplo de Next.js.
El script de puntuación devuelve 401 unauthorized o 403 forbiddenUn 401 significa que falta la clave o está revocada; un 403 significa que es publicable o que le falta el permiso. Usa una clave secreta (ik_sk_) con el permiso submissions:read.
El envío está en el panel pero no llegó ningún email ni webhookSe guardó como spam. El spam no envía nada. Ábrelo con el filtro Spam para ver los motivos y márcalo como no spam para que se envíen su email y sus webhooks.

Preguntas

¿Te atascaste en un paso o quieres una guía para tu caso? Escribe a hello@ikiform.com.