diff --git a/Bonsai.System/IO/ReadAllText.cs b/Bonsai.System/IO/ReadAllText.cs
new file mode 100644
index 00000000..1479c49f
--- /dev/null
+++ b/Bonsai.System/IO/ReadAllText.cs
@@ -0,0 +1,36 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Reactive.Linq;
+
+namespace Bonsai.IO
+{
+ ///
+ /// Represents an operator that opens a text file, returns a single string with all
+ /// lines in the file, and then closes the file.
+ ///
+ [DefaultProperty(nameof(Path))]
+ [Description("Opens a text file, returns a single string with all lines in the file, and then closes the file.")]
+ public class ReadAllText : Source
+ {
+ ///
+ /// Gets or sets the relative or absolute path of the file to open for reading.
+ ///
+ [Description("The relative or absolute path of the file to open for reading.")]
+ [Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)]
+ public string Path { get; set; }
+
+ ///
+ /// Generates an observable sequence that opens the text file, returns a single string
+ /// with all lines in the file, and then closes the file.
+ ///
+ ///
+ /// A sequence containing a single string with all lines in the file.
+ ///
+ public override IObservable Generate()
+ {
+ var path = Path;
+ return Observable.Defer(() => Observable.Return(File.ReadAllText(path)));
+ }
+ }
+}
diff --git a/Bonsai.System/IO/WriteAllText.cs b/Bonsai.System/IO/WriteAllText.cs
new file mode 100644
index 00000000..c61ffd9a
--- /dev/null
+++ b/Bonsai.System/IO/WriteAllText.cs
@@ -0,0 +1,64 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Reactive.Linq;
+
+namespace Bonsai.IO
+{
+ ///
+ /// Represents an operator that opens a text file, writes the source string to
+ /// the file, and then closes the file.
+ ///
+ [DefaultProperty(nameof(Path))]
+ [Description("Creates a new file, writes the source string to the file, and then closes the file.")]
+ public class WriteAllText : Sink
+ {
+ ///
+ /// Gets or sets the relative or absolute path of the file to open for writing.
+ ///
+ [Description("The relative or absolute path of the file to open for writing.")]
+ [Editor("Bonsai.Design.SaveFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)]
+ public string Path { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the output file should be overwritten if it already exists.
+ ///
+ [Description("Indicates whether the output file should be overwritten if it already exists.")]
+ public bool Overwrite { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether text should be appended to the output file if it already exists.
+ ///
+ [Description("Indicates whether text should be appended to the output file if it already exists.")]
+ public bool Append { get; set; }
+
+ ///
+ /// Creates a new file, writes the string in the observable sequence to the file,
+ /// and then closes the file.
+ ///
+ ///
+ /// The sequence containing the string to write to the file.
+ ///
+ ///
+ /// An observable sequence that is identical to the sequence
+ /// but where there is an additional side effect of writing the string to the file.
+ ///
+ public override IObservable Process(IObservable source)
+ {
+ var path = Path;
+ var overwrite = Overwrite;
+ var append = Append;
+ return source.Do(contents =>
+ {
+ PathHelper.EnsureDirectory(path);
+ if (File.Exists(path) && !overwrite && !append)
+ {
+ throw new IOException($"The file '{path}' already exists.");
+ }
+
+ using var writer = new StreamWriter(path, append);
+ writer.Write(contents);
+ });
+ }
+ }
+}