Write Safely
A write changes something outside the run: a Slack message, a Linear issue, or a row in an API. Networks fail, and runs can run again after a repair. Plan each write so that a second attempt is safe.
Use a stable effect key
Section titled “Use a stable effect key”Each write needs an effectKey. Build it from values that stay the same for the same logical change:
| Trigger | Good key | Bad key |
|---|---|---|
| An event | `issue:${event.id}` | `issue:${Date.now()}` |
| A schedule | `report:${event.scheduledAt}` | `report:${Math.random()}` |
| An item in a list | `notify:${order.id}` | `notify:${index}` |
Make writes safe to repeat
Section titled “Make writes safe to repeat”Use one or more of these methods:
- Use the provider’s idempotency key. Many APIs accept an
Idempotency-Keyheader. Send the effect key as its value. - Read before you write. Look for an item from an earlier attempt. For example, find an issue with the event ID in its title.
- Use update instead of create. An update with the same values has the same result each time.
- Save progress in a checkpoint. Record which items are done, so the next run skips them.
Handle an unknown result
Section titled “Handle an unknown result”A timeout or a lost connection does not mean that the write failed. The provider can have done it.
- Do not send the write again at once.
- Read the destination to find out what happened.
- If you cannot find out, fail the run with a clear message. A person or the repair process can then check it.
const created = await ctx.write.call(createIssue, issueSchema, { effectKey: `issue:${event.id}` });if (created.isErr()) { if (created.error.code === "timeout" || created.error.code === "network") { return err(new TypeApiError(`Issue for ${event.id} has an unknown result. Check Linear before you retry.`, "network")); } return err(created.error);}Record important steps
Section titled “Record important steps”ctx.mutativeAction records a step in run history before and after it runs:
const created = await ctx.mutativeAction( () => ctx.write.call(createIssue, issueSchema, { effectKey: `issue:${event.id}` }), { id: "create-issue", message: `Create a Linear issue for ${event.id}` }, (issue) => ({ message: `Created ${issue.id}`, data: { id: issue.id } }),);- Run history shows what the code tried to do and what happened.
- Check the provider response inside the action, so the log shows a real success.
- Action logs do not retry, and they do not undo earlier steps.
- If the
afterlog fails to save, the error says “Do not repeat the action”. The write can be complete.
Order of operations
Section titled “Order of operations”Type saves the checkpoint and the output only after the method succeeds. A write happens at the time that you call it. Thus:
- A write that succeeds, followed by a failure later in the method, is not undone.
- The next run starts from the old checkpoint, and can try the same write again.
- After a failure, the repair’s verification run uses the same event again, with real access. See Reconcile before verification.
Put the most important write last, or make every write safe to repeat.
Tests do not write
Section titled “Tests do not write”In fixtures, Type gives your code the responses that you supply. No real write happens. Fixtures prove how your code handles a response. They do not prove that the provider accepts the write.